puzzlefs_lib/
extractor.rs

1use crate::format::InodeMode;
2use crate::oci::Image;
3use crate::reader::{PuzzleFS, WalkPuzzleFS};
4use log::info;
5use nix::sys::stat::{makedev, mknod, Mode, SFlag};
6use nix::unistd::{chown, mkfifo, symlinkat, Gid, Uid};
7use std::collections::HashMap;
8use std::ffi::OsStr;
9use std::fs::Permissions;
10use std::os::unix::ffi::OsStrExt;
11use std::os::unix::fs::PermissionsExt;
12use std::path::{Component, Path, PathBuf};
13use std::{fs, io};
14
15fn runs_privileged() -> bool {
16    Uid::effective().is_root()
17}
18
19fn safe_path(dir: &Path, image_path: &Path) -> anyhow::Result<PathBuf> {
20    // need to be a bit careful here about paths in the case of malicious images so we don't write
21    // things outside where we're supposed to. Bad cases are paths like "/../../.." or images
22    // /usr/bin -> /bin and files in /usr/bin, we shouldn't write files anywhere outside the target
23    // dir.
24
25    let mut buf = PathBuf::new();
26    buf.push(dir);
27    let mut level = 1;
28
29    for component in image_path.components() {
30        match component {
31            Component::Prefix(..) => bail!("Path prefix not understood"), // "Does not occur on Unix."
32            Component::RootDir => {}
33            Component::CurDir => {}
34            Component::Normal(c) => {
35                buf.push(c);
36                level += 1;
37
38                // make sure this isn't a symlink
39                match fs::symlink_metadata(&buf) {
40                    Ok(md) => {
41                        if md.file_type().is_symlink() {
42                            bail!("symlink prefixes are not allowed: {:#?}", buf)
43                        }
44                    }
45                    Err(e) => {
46                        if e.kind() != io::ErrorKind::NotFound {
47                            bail!("problem accessing path component {:#?}: {}", buf, e)
48                        }
49
50                        // we render each dir, so the first ENOENT should be the lowest path. could
51                        // maybe double check this if we really felt it was necessary...
52                        return Ok(buf);
53                    }
54                }
55            }
56            Component::ParentDir => {
57                level -= 1;
58                if level <= 0 {
59                    bail!("image path escapes extract dir: {:#?}", image_path)
60                }
61                buf.pop();
62            }
63        }
64    }
65
66    Ok(buf)
67}
68
69pub fn extract_rootfs(oci_dir: &str, tag: &str, extract_dir: &str) -> anyhow::Result<()> {
70    let oci_dir = Path::new(oci_dir);
71    let image = Image::open(oci_dir)?;
72    let dir = Path::new(extract_dir);
73    fs::create_dir_all(dir)?;
74    let mut pfs = PuzzleFS::open(image, tag, None)?;
75    let mut walker = WalkPuzzleFS::walk(&mut pfs)?;
76    let mut host_to_pfs = HashMap::<crate::format::Ino, PathBuf>::new();
77
78    walker.try_for_each(|de| -> anyhow::Result<()> {
79        let dir_entry = de?;
80        let path = safe_path(dir, &dir_entry.path)?;
81        let mut is_symlink = false;
82        info!("extracting {:#?}", path);
83        if let Some(existing_path) = host_to_pfs.get(&dir_entry.inode.ino) {
84            fs::hard_link(existing_path, &path)?;
85            return Ok(());
86        }
87        host_to_pfs.insert(dir_entry.inode.ino, path.clone());
88
89        match dir_entry.inode.mode {
90            InodeMode::File { .. } => {
91                let mut reader = dir_entry.open()?;
92                let mut f = fs::File::create(&path)?;
93                io::copy(&mut reader, &mut f)?;
94            }
95            InodeMode::Dir { .. } => fs::create_dir_all(&path)?,
96            // TODO: fix all the hard coded modes when we have modes
97            InodeMode::Fifo => {
98                mkfifo(&path, Mode::S_IRWXU)?;
99            }
100            InodeMode::Chr { major, minor } => {
101                mknod(&path, SFlag::S_IFCHR, Mode::S_IRWXU, makedev(major, minor))?;
102            }
103            InodeMode::Blk { major, minor } => {
104                mknod(&path, SFlag::S_IFBLK, Mode::S_IRWXU, makedev(major, minor))?;
105            }
106            InodeMode::Lnk => {
107                let target = dir_entry.inode.symlink_target()?;
108                is_symlink = true;
109                symlinkat(target, None, &path)?;
110            }
111            InodeMode::Sock => {
112                todo!();
113            }
114            InodeMode::Wht => {
115                todo!();
116            }
117            _ => {
118                bail!("bad inode mode {:#?}", dir_entry.inode.mode)
119            }
120        }
121        if let Some(x) = dir_entry.inode.additional {
122            for x in &x.xattrs {
123                xattr::set(&path, OsStr::from_bytes(&x.key), &x.val)?;
124            }
125        }
126
127        // trying to change permissions for a symlink would follow the symlink and we might not have extracted the target yet
128        // anyway, symlink permissions are not used in Linux (although they are used in macOS and FreeBSD)
129        if !is_symlink {
130            std::fs::set_permissions(
131                &path,
132                Permissions::from_mode(dir_entry.inode.permissions.into()),
133            )?;
134        }
135
136        if runs_privileged() {
137            chown(
138                &path,
139                Some(Uid::from_raw(dir_entry.inode.uid)),
140                Some(Gid::from_raw(dir_entry.inode.gid)),
141            )?;
142        }
143
144        Ok(())
145    })?;
146    Ok(())
147}
148
149#[cfg(test)]
150mod tests {
151    use tempfile::{tempdir, TempDir};
152
153    use std::fs::File;
154
155    use crate::builder::build_test_fs;
156    use std::os::unix::fs::MetadataExt;
157    use walkdir::WalkDir;
158
159    use super::*;
160
161    #[test]
162    fn test_extracted_xattrs() {
163        let dir = TempDir::new_in(".").unwrap();
164        let oci_dir = dir.path().join("oci");
165        let image = Image::new(&oci_dir).unwrap();
166        let rootfs = dir.path().join("rootfs");
167        let extract_dir = TempDir::new_in(".").unwrap();
168
169        let foo = rootfs.join("foo");
170        let bar = rootfs.join("bar");
171
172        let mut file_attributes = HashMap::<String, Vec<u8>>::new();
173        file_attributes.insert("user.meshuggah".to_string(), b"rocks".to_vec());
174        file_attributes.insert("user.nothing".to_string(), b"".to_vec());
175
176        // test directory, file types. we should probably also test "other" types, but on fifos and
177        // symlinks on linux xattrs aren't allowed, so we just punt for now. maybe when 5.8 is more
178        // prevalent, we can use mknod c 0 0?
179        fs::create_dir_all(&foo).unwrap();
180        fs::write(&bar, b"bar").unwrap();
181
182        // set some xattrs
183        for f in [&foo, &bar] {
184            for (key, val) in &file_attributes {
185                xattr::set(f, key, val).unwrap();
186                xattr::set(f, key, val).unwrap();
187            }
188        }
189
190        build_test_fs(&rootfs, &image, "test").unwrap();
191
192        extract_rootfs(
193            oci_dir.to_str().unwrap(),
194            "test",
195            extract_dir.path().to_str().unwrap(),
196        )
197        .unwrap();
198
199        let ents = WalkDir::new(&extract_dir)
200            .contents_first(false)
201            .follow_links(false)
202            .same_file_system(true)
203            .sort_by(|a, b| a.file_name().cmp(b.file_name()))
204            .into_iter()
205            .collect::<Result<Vec<walkdir::DirEntry>, walkdir::Error>>()
206            .unwrap();
207
208        // the first directory is extract_dir, we don't check xattrs for it
209        for ent in ents.into_iter().skip(1) {
210            for (key, val) in &file_attributes {
211                let attribute = xattr::get(ent.path(), key);
212                println!(
213                    "path: {:?} key: {:?} attribute: {:?}",
214                    ent.path(),
215                    key,
216                    attribute
217                );
218                assert!(attribute.unwrap().as_ref().unwrap() == val);
219            }
220        }
221    }
222
223    #[test]
224    fn test_permissions() {
225        let dir = tempdir().unwrap();
226        let oci_dir = dir.path().join("oci");
227        let image = Image::new(&oci_dir).unwrap();
228        let rootfs = dir.path().join("rootfs");
229        let extract_dir = tempdir().unwrap();
230        const TESTED_PERMISSION: u32 = 0o7777;
231
232        let foo = rootfs.join("foo");
233
234        fs::create_dir_all(&rootfs).unwrap();
235        fs::write(&foo, b"foo").unwrap();
236
237        std::fs::set_permissions(foo, Permissions::from_mode(TESTED_PERMISSION)).unwrap();
238
239        build_test_fs(&rootfs, &image, "test").unwrap();
240
241        extract_rootfs(
242            oci_dir.to_str().unwrap(),
243            "test",
244            extract_dir.path().to_str().unwrap(),
245        )
246        .unwrap();
247
248        let extracted_path = extract_dir.path().join("foo");
249        let f = File::open(extracted_path).unwrap();
250        let metadata = f.metadata().unwrap();
251
252        assert_eq!(metadata.permissions().mode() & 0xFFF, TESTED_PERMISSION);
253    }
254
255    #[test]
256    fn test_hardlink_extraction() {
257        let dir = tempdir().unwrap();
258        let oci_dir = dir.path().join("oci");
259        let image = Image::new(&oci_dir).unwrap();
260        let rootfs = dir.path().join("rootfs");
261        let extract_dir = tempdir().unwrap();
262
263        let foo = rootfs.join("foo");
264        let bar = rootfs.join("bar");
265
266        fs::create_dir_all(&rootfs).unwrap();
267        fs::write(&foo, b"foo").unwrap();
268
269        fs::hard_link(&foo, &bar).unwrap();
270
271        assert_eq!(
272            fs::metadata(&foo).unwrap().ino(),
273            fs::metadata(&bar).unwrap().ino()
274        );
275
276        build_test_fs(&rootfs, &image, "test").unwrap();
277
278        extract_rootfs(
279            oci_dir.to_str().unwrap(),
280            "test",
281            extract_dir.path().to_str().unwrap(),
282        )
283        .unwrap();
284
285        let foo = extract_dir.path().join("foo");
286        let bar = extract_dir.path().join("bar");
287
288        assert_eq!(
289            fs::metadata(foo).unwrap().ino(),
290            fs::metadata(bar).unwrap().ino()
291        );
292    }
293
294    #[test]
295    fn test_empty_file() {
296        let dir = tempdir().unwrap();
297        let oci_dir = dir.path().join("oci");
298        let image = Image::new(&oci_dir).unwrap();
299        let rootfs = dir.path().join("rootfs");
300        let foo = rootfs.join("foo");
301        let extract_dir = tempdir().unwrap();
302
303        fs::create_dir_all(&rootfs).unwrap();
304        std::fs::File::create(foo).unwrap();
305
306        build_test_fs(&rootfs, &image, "test").unwrap();
307
308        extract_rootfs(
309            oci_dir.to_str().unwrap(),
310            "test",
311            extract_dir.path().to_str().unwrap(),
312        )
313        .unwrap();
314        let extracted_foo = extract_dir.path().join("foo");
315        assert_eq!(extracted_foo.metadata().unwrap().len(), 0);
316    }
317}