solana_runtime/
hardened_unpack.rs

1use {
2    bzip2::bufread::BzDecoder,
3    log::*,
4    rand::{thread_rng, Rng},
5    solana_sdk::genesis_config::{GenesisConfig, DEFAULT_GENESIS_ARCHIVE, DEFAULT_GENESIS_FILE},
6    std::{
7        collections::HashMap,
8        fs::{self, File},
9        io::{BufReader, Read},
10        path::{
11            Component::{self, CurDir, Normal},
12            Path, PathBuf,
13        },
14        time::Instant,
15    },
16    tar::{
17        Archive,
18        EntryType::{Directory, GNUSparse, Regular},
19    },
20    thiserror::Error,
21};
22
23#[derive(Error, Debug)]
24pub enum UnpackError {
25    #[error("IO error: {0}")]
26    Io(#[from] std::io::Error),
27    #[error("Archive error: {0}")]
28    Archive(String),
29}
30
31pub type Result<T> = std::result::Result<T, UnpackError>;
32
33// 64 TiB; some safe margin to the max 128 TiB in amd64 linux userspace VmSize
34// (ref: https://unix.stackexchange.com/a/386555/364236)
35// note that this is directly related to the mmaped data size
36// so protect against insane value
37// This is the file size including holes for sparse files
38const MAX_SNAPSHOT_ARCHIVE_UNPACKED_APPARENT_SIZE: u64 = 64 * 1024 * 1024 * 1024 * 1024;
39
40// 4 TiB;
41// This is the actually consumed disk usage for sparse files
42const MAX_SNAPSHOT_ARCHIVE_UNPACKED_ACTUAL_SIZE: u64 = 4 * 1024 * 1024 * 1024 * 1024;
43
44const MAX_SNAPSHOT_ARCHIVE_UNPACKED_COUNT: u64 = 5_000_000;
45pub const MAX_GENESIS_ARCHIVE_UNPACKED_SIZE: u64 = 10 * 1024 * 1024; // 10 MiB
46const MAX_GENESIS_ARCHIVE_UNPACKED_COUNT: u64 = 100;
47
48fn checked_total_size_sum(total_size: u64, entry_size: u64, limit_size: u64) -> Result<u64> {
49    trace!(
50        "checked_total_size_sum: {} + {} < {}",
51        total_size,
52        entry_size,
53        limit_size,
54    );
55    let total_size = total_size.saturating_add(entry_size);
56    if total_size > limit_size {
57        return Err(UnpackError::Archive(format!(
58            "too large archive: {} than limit: {}",
59            total_size, limit_size,
60        )));
61    }
62    Ok(total_size)
63}
64
65fn checked_total_count_increment(total_count: u64, limit_count: u64) -> Result<u64> {
66    let total_count = total_count + 1;
67    if total_count > limit_count {
68        return Err(UnpackError::Archive(format!(
69            "too many files in snapshot: {:?}",
70            total_count
71        )));
72    }
73    Ok(total_count)
74}
75
76fn check_unpack_result(unpack_result: bool, path: String) -> Result<()> {
77    if !unpack_result {
78        return Err(UnpackError::Archive(format!(
79            "failed to unpack: {:?}",
80            path
81        )));
82    }
83    Ok(())
84}
85
86#[derive(Debug, PartialEq, Eq)]
87pub enum UnpackPath<'a> {
88    Valid(&'a Path),
89    Ignore,
90    Invalid,
91}
92
93fn unpack_archive<'a, A, C, D>(
94    archive: &mut Archive<A>,
95    apparent_limit_size: u64,
96    actual_limit_size: u64,
97    limit_count: u64,
98    mut entry_checker: C, // checks if entry is valid
99    entry_processor: D,   // processes entry after setting permissions
100) -> Result<()>
101where
102    A: Read,
103    C: FnMut(&[&str], tar::EntryType) -> UnpackPath<'a>,
104    D: Fn(PathBuf),
105{
106    let mut apparent_total_size: u64 = 0;
107    let mut actual_total_size: u64 = 0;
108    let mut total_count: u64 = 0;
109
110    let mut total_entries = 0;
111    let mut last_log_update = Instant::now();
112    for entry in archive.entries()? {
113        let mut entry = entry?;
114        let path = entry.path()?;
115        let path_str = path.display().to_string();
116
117        // Although the `tar` crate safely skips at the actual unpacking, fail
118        // first by ourselves when there are odd paths like including `..` or /
119        // for our clearer pattern matching reasoning:
120        //   https://docs.rs/tar/0.4.26/src/tar/entry.rs.html#371
121        let parts = path.components().map(|p| match p {
122            CurDir => Some("."),
123            Normal(c) => c.to_str(),
124            _ => None, // Prefix (for Windows) and RootDir are forbidden
125        });
126
127        // Reject old-style BSD directory entries that aren't explicitly tagged as directories
128        let legacy_dir_entry =
129            entry.header().as_ustar().is_none() && entry.path_bytes().ends_with(b"/");
130        let kind = entry.header().entry_type();
131        let reject_legacy_dir_entry = legacy_dir_entry && (kind != Directory);
132
133        if parts.clone().any(|p| p.is_none()) || reject_legacy_dir_entry {
134            return Err(UnpackError::Archive(format!(
135                "invalid path found: {:?}",
136                path_str
137            )));
138        }
139
140        let parts: Vec<_> = parts.map(|p| p.unwrap()).collect();
141        let unpack_dir = match entry_checker(parts.as_slice(), kind) {
142            UnpackPath::Invalid => {
143                return Err(UnpackError::Archive(format!(
144                    "extra entry found: {:?} {:?}",
145                    path_str,
146                    entry.header().entry_type(),
147                )));
148            }
149            UnpackPath::Ignore => {
150                continue;
151            }
152            UnpackPath::Valid(unpack_dir) => unpack_dir,
153        };
154
155        apparent_total_size = checked_total_size_sum(
156            apparent_total_size,
157            entry.header().size()?,
158            apparent_limit_size,
159        )?;
160        actual_total_size = checked_total_size_sum(
161            actual_total_size,
162            entry.header().entry_size()?,
163            actual_limit_size,
164        )?;
165        total_count = checked_total_count_increment(total_count, limit_count)?;
166
167        let target = sanitize_path(&entry.path()?, unpack_dir)?; // ? handles file system errors
168        if target.is_none() {
169            continue; // skip it
170        }
171        let target = target.unwrap();
172
173        let unpack = entry.unpack(target);
174        check_unpack_result(unpack.map(|_unpack| true)?, path_str)?;
175
176        // Sanitize permissions.
177        let mode = match entry.header().entry_type() {
178            GNUSparse | Regular => 0o644,
179            _ => 0o755,
180        };
181        let entry_path_buf = unpack_dir.join(entry.path()?);
182        set_perms(&entry_path_buf, mode)?;
183
184        // Process entry after setting permissions
185        entry_processor(entry_path_buf);
186
187        total_entries += 1;
188        let now = Instant::now();
189        if now.duration_since(last_log_update).as_secs() >= 10 {
190            info!("unpacked {} entries so far...", total_entries);
191            last_log_update = now;
192        }
193    }
194    info!("unpacked {} entries total", total_entries);
195
196    return Ok(());
197
198    #[cfg(unix)]
199    fn set_perms(dst: &Path, mode: u32) -> std::io::Result<()> {
200        use std::os::unix::fs::PermissionsExt;
201
202        let perm = fs::Permissions::from_mode(mode as _);
203        fs::set_permissions(dst, perm)
204    }
205
206    #[cfg(windows)]
207    fn set_perms(dst: &Path, _mode: u32) -> std::io::Result<()> {
208        let mut perm = fs::metadata(dst)?.permissions();
209        perm.set_readonly(false);
210        fs::set_permissions(dst, perm)
211    }
212}
213
214// return Err on file system error
215// return Some(path) if path is good
216// return None if we should skip this file
217fn sanitize_path(entry_path: &Path, dst: &Path) -> Result<Option<PathBuf>> {
218    // We cannot call unpack_in because it errors if we try to use 2 account paths.
219    // So, this code is borrowed from unpack_in
220    // ref: https://docs.rs/tar/*/tar/struct.Entry.html#method.unpack_in
221    let mut file_dst = dst.to_path_buf();
222    const SKIP: Result<Option<PathBuf>> = Ok(None);
223    {
224        let path = entry_path;
225        for part in path.components() {
226            match part {
227                // Leading '/' characters, root paths, and '.'
228                // components are just ignored and treated as "empty
229                // components"
230                Component::Prefix(..) | Component::RootDir | Component::CurDir => continue,
231
232                // If any part of the filename is '..', then skip over
233                // unpacking the file to prevent directory traversal
234                // security issues.  See, e.g.: CVE-2001-1267,
235                // CVE-2002-0399, CVE-2005-1918, CVE-2007-4131
236                Component::ParentDir => return SKIP,
237
238                Component::Normal(part) => file_dst.push(part),
239            }
240        }
241    }
242
243    // Skip cases where only slashes or '.' parts were seen, because
244    // this is effectively an empty filename.
245    if *dst == *file_dst {
246        return SKIP;
247    }
248
249    // Skip entries without a parent (i.e. outside of FS root)
250    let parent = match file_dst.parent() {
251        Some(p) => p,
252        None => return SKIP,
253    };
254
255    fs::create_dir_all(parent)?;
256
257    // Here we are different than untar_in. The code for tar::unpack_in internally calling unpack is a little different.
258    // ignore return value here
259    validate_inside_dst(dst, parent)?;
260    let target = parent.join(entry_path.file_name().unwrap());
261
262    Ok(Some(target))
263}
264
265// copied from:
266// https://github.com/alexcrichton/tar-rs/blob/d90a02f582c03dfa0fd11c78d608d0974625ae5d/src/entry.rs#L781
267fn validate_inside_dst(dst: &Path, file_dst: &Path) -> Result<PathBuf> {
268    // Abort if target (canonical) parent is outside of `dst`
269    let canon_parent = file_dst.canonicalize().map_err(|err| {
270        UnpackError::Archive(format!(
271            "{} while canonicalizing {}",
272            err,
273            file_dst.display()
274        ))
275    })?;
276    let canon_target = dst.canonicalize().map_err(|err| {
277        UnpackError::Archive(format!("{} while canonicalizing {}", err, dst.display()))
278    })?;
279    if !canon_parent.starts_with(&canon_target) {
280        return Err(UnpackError::Archive(format!(
281            "trying to unpack outside of destination path: {}",
282            canon_target.display()
283        )));
284    }
285    Ok(canon_target)
286}
287
288/// Map from AppendVec file name to unpacked file system location
289pub type UnpackedAppendVecMap = HashMap<String, PathBuf>;
290
291// select/choose only 'index' out of each # of 'divisions' of total items.
292pub struct ParallelSelector {
293    pub index: usize,
294    pub divisions: usize,
295}
296
297impl ParallelSelector {
298    pub fn select_index(&self, index: usize) -> bool {
299        index % self.divisions == self.index
300    }
301}
302
303/// Unpacks snapshot and collects AppendVec file names & paths
304pub fn unpack_snapshot<A: Read>(
305    archive: &mut Archive<A>,
306    ledger_dir: &Path,
307    account_paths: &[PathBuf],
308    parallel_selector: Option<ParallelSelector>,
309) -> Result<UnpackedAppendVecMap> {
310    let mut unpacked_append_vec_map = UnpackedAppendVecMap::new();
311
312    unpack_snapshot_with_processors(
313        archive,
314        ledger_dir,
315        account_paths,
316        parallel_selector,
317        |file, path| {
318            unpacked_append_vec_map.insert(file.to_string(), path.join("accounts").join(file));
319        },
320        |_| {},
321    )
322    .map(|_| unpacked_append_vec_map)
323}
324
325/// Unpacks snapshots and sends entry file paths through the `sender` channel
326pub fn streaming_unpack_snapshot<A: Read>(
327    archive: &mut Archive<A>,
328    ledger_dir: &Path,
329    account_paths: &[PathBuf],
330    parallel_selector: Option<ParallelSelector>,
331    sender: &crossbeam_channel::Sender<PathBuf>,
332) -> Result<()> {
333    unpack_snapshot_with_processors(
334        archive,
335        ledger_dir,
336        account_paths,
337        parallel_selector,
338        |_, _| {},
339        |entry_path_buf| {
340            if entry_path_buf.is_file() {
341                sender.send(entry_path_buf).unwrap();
342            }
343        },
344    )
345}
346
347fn unpack_snapshot_with_processors<A, F, G>(
348    archive: &mut Archive<A>,
349    ledger_dir: &Path,
350    account_paths: &[PathBuf],
351    parallel_selector: Option<ParallelSelector>,
352    mut accounts_path_processor: F,
353    entry_processor: G,
354) -> Result<()>
355where
356    A: Read,
357    F: FnMut(&str, &Path),
358    G: Fn(PathBuf),
359{
360    assert!(!account_paths.is_empty());
361    let mut i = 0;
362
363    unpack_archive(
364        archive,
365        MAX_SNAPSHOT_ARCHIVE_UNPACKED_APPARENT_SIZE,
366        MAX_SNAPSHOT_ARCHIVE_UNPACKED_ACTUAL_SIZE,
367        MAX_SNAPSHOT_ARCHIVE_UNPACKED_COUNT,
368        |parts, kind| {
369            if is_valid_snapshot_archive_entry(parts, kind) {
370                i += 1;
371                match &parallel_selector {
372                    Some(parallel_selector) => {
373                        if !parallel_selector.select_index(i - 1) {
374                            return UnpackPath::Ignore;
375                        }
376                    }
377                    None => {}
378                };
379                if let ["accounts", file] = parts {
380                    // Randomly distribute the accounts files about the available `account_paths`,
381                    let path_index = thread_rng().gen_range(0, account_paths.len());
382                    match account_paths
383                        .get(path_index)
384                        .map(|path_buf| path_buf.as_path())
385                    {
386                        Some(path) => {
387                            accounts_path_processor(*file, path);
388                            UnpackPath::Valid(path)
389                        }
390                        None => UnpackPath::Invalid,
391                    }
392                } else {
393                    UnpackPath::Valid(ledger_dir)
394                }
395            } else {
396                UnpackPath::Invalid
397            }
398        },
399        entry_processor,
400    )
401}
402
403fn all_digits(v: &str) -> bool {
404    if v.is_empty() {
405        return false;
406    }
407    for x in v.chars() {
408        if !x.is_ascii_digit() {
409            return false;
410        }
411    }
412    true
413}
414
415fn like_storage(v: &str) -> bool {
416    let mut periods = 0;
417    let mut saw_numbers = false;
418    for x in v.chars() {
419        if !x.is_ascii_digit() {
420            if x == '.' {
421                if periods > 0 || !saw_numbers {
422                    return false;
423                }
424                saw_numbers = false;
425                periods += 1;
426            } else {
427                return false;
428            }
429        } else {
430            saw_numbers = true;
431        }
432    }
433    saw_numbers && periods == 1
434}
435
436fn is_valid_snapshot_archive_entry(parts: &[&str], kind: tar::EntryType) -> bool {
437    match (parts, kind) {
438        (["version"], Regular) => true,
439        (["accounts"], Directory) => true,
440        (["accounts", file], GNUSparse) if like_storage(file) => true,
441        (["accounts", file], Regular) if like_storage(file) => true,
442        (["snapshots"], Directory) => true,
443        (["snapshots", "status_cache"], GNUSparse) => true,
444        (["snapshots", "status_cache"], Regular) => true,
445        (["snapshots", dir, file], GNUSparse) if all_digits(dir) && all_digits(file) => true,
446        (["snapshots", dir, file], Regular) if all_digits(dir) && all_digits(file) => true,
447        (["snapshots", dir], Directory) if all_digits(dir) => true,
448        _ => false,
449    }
450}
451
452pub fn open_genesis_config(
453    ledger_path: &Path,
454    max_genesis_archive_unpacked_size: u64,
455) -> GenesisConfig {
456    GenesisConfig::load(ledger_path).unwrap_or_else(|load_err| {
457        let genesis_package = ledger_path.join(DEFAULT_GENESIS_ARCHIVE);
458        unpack_genesis_archive(
459            &genesis_package,
460            ledger_path,
461            max_genesis_archive_unpacked_size,
462        )
463        .unwrap_or_else(|unpack_err| {
464            warn!(
465                "Failed to open ledger genesis_config at {:?}: {}, {}",
466                ledger_path, load_err, unpack_err,
467            );
468            std::process::exit(1);
469        });
470
471        // loading must succeed at this moment
472        GenesisConfig::load(ledger_path).unwrap()
473    })
474}
475
476pub fn unpack_genesis_archive(
477    archive_filename: &Path,
478    destination_dir: &Path,
479    max_genesis_archive_unpacked_size: u64,
480) -> std::result::Result<(), UnpackError> {
481    info!("Extracting {:?}...", archive_filename);
482    let extract_start = Instant::now();
483
484    fs::create_dir_all(destination_dir)?;
485    let tar_bz2 = File::open(archive_filename)?;
486    let tar = BzDecoder::new(BufReader::new(tar_bz2));
487    let mut archive = Archive::new(tar);
488    unpack_genesis(
489        &mut archive,
490        destination_dir,
491        max_genesis_archive_unpacked_size,
492    )?;
493    info!(
494        "Extracted {:?} in {:?}",
495        archive_filename,
496        Instant::now().duration_since(extract_start)
497    );
498    Ok(())
499}
500
501fn unpack_genesis<A: Read>(
502    archive: &mut Archive<A>,
503    unpack_dir: &Path,
504    max_genesis_archive_unpacked_size: u64,
505) -> Result<()> {
506    unpack_archive(
507        archive,
508        max_genesis_archive_unpacked_size,
509        max_genesis_archive_unpacked_size,
510        MAX_GENESIS_ARCHIVE_UNPACKED_COUNT,
511        |p, k| is_valid_genesis_archive_entry(unpack_dir, p, k),
512        |_| {},
513    )
514}
515
516fn is_valid_genesis_archive_entry<'a>(
517    unpack_dir: &'a Path,
518    parts: &[&str],
519    kind: tar::EntryType,
520) -> UnpackPath<'a> {
521    trace!("validating: {:?} {:?}", parts, kind);
522    #[allow(clippy::match_like_matches_macro)]
523    match (parts, kind) {
524        ([DEFAULT_GENESIS_FILE], GNUSparse) => UnpackPath::Valid(unpack_dir),
525        ([DEFAULT_GENESIS_FILE], Regular) => UnpackPath::Valid(unpack_dir),
526        (["rocksdb"], Directory) => UnpackPath::Ignore,
527        (["rocksdb", _], GNUSparse) => UnpackPath::Ignore,
528        (["rocksdb", _], Regular) => UnpackPath::Ignore,
529        (["rocksdb_fifo"], Directory) => UnpackPath::Ignore,
530        (["rocksdb_fifo", _], GNUSparse) => UnpackPath::Ignore,
531        (["rocksdb_fifo", _], Regular) => UnpackPath::Ignore,
532        _ => UnpackPath::Invalid,
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use {
539        super::*,
540        assert_matches::assert_matches,
541        tar::{Builder, Header},
542    };
543
544    #[test]
545    fn test_archive_is_valid_entry() {
546        assert!(is_valid_snapshot_archive_entry(
547            &["snapshots"],
548            tar::EntryType::Directory
549        ));
550        assert!(!is_valid_snapshot_archive_entry(
551            &["snapshots", ""],
552            tar::EntryType::Directory
553        ));
554        assert!(is_valid_snapshot_archive_entry(
555            &["snapshots", "3"],
556            tar::EntryType::Directory
557        ));
558        assert!(is_valid_snapshot_archive_entry(
559            &["snapshots", "3", "3"],
560            tar::EntryType::Regular
561        ));
562        assert!(is_valid_snapshot_archive_entry(
563            &["version"],
564            tar::EntryType::Regular
565        ));
566        assert!(is_valid_snapshot_archive_entry(
567            &["accounts"],
568            tar::EntryType::Directory
569        ));
570        assert!(!is_valid_snapshot_archive_entry(
571            &["accounts", ""],
572            tar::EntryType::Regular
573        ));
574
575        assert!(!is_valid_snapshot_archive_entry(
576            &["snapshots"],
577            tar::EntryType::Regular
578        ));
579        assert!(!is_valid_snapshot_archive_entry(
580            &["snapshots", "x0"],
581            tar::EntryType::Directory
582        ));
583        assert!(!is_valid_snapshot_archive_entry(
584            &["snapshots", "0x"],
585            tar::EntryType::Directory
586        ));
587        assert!(!is_valid_snapshot_archive_entry(
588            &["snapshots", "①"],
589            tar::EntryType::Directory
590        ));
591        assert!(!is_valid_snapshot_archive_entry(
592            &["snapshots", "0", "aa"],
593            tar::EntryType::Regular
594        ));
595        assert!(!is_valid_snapshot_archive_entry(
596            &["aaaa"],
597            tar::EntryType::Regular
598        ));
599    }
600
601    #[test]
602    fn test_valid_snapshot_accounts() {
603        solana_logger::setup();
604        assert!(is_valid_snapshot_archive_entry(
605            &["accounts", "0.0"],
606            tar::EntryType::Regular
607        ));
608        assert!(is_valid_snapshot_archive_entry(
609            &["accounts", "01829.077"],
610            tar::EntryType::Regular
611        ));
612
613        assert!(!is_valid_snapshot_archive_entry(
614            &["accounts", "1.2.34"],
615            tar::EntryType::Regular
616        ));
617        assert!(!is_valid_snapshot_archive_entry(
618            &["accounts", "12."],
619            tar::EntryType::Regular
620        ));
621        assert!(!is_valid_snapshot_archive_entry(
622            &["accounts", ".12"],
623            tar::EntryType::Regular
624        ));
625        assert!(!is_valid_snapshot_archive_entry(
626            &["accounts", "0x0"],
627            tar::EntryType::Regular
628        ));
629        assert!(!is_valid_snapshot_archive_entry(
630            &["accounts", "abc"],
631            tar::EntryType::Regular
632        ));
633        assert!(!is_valid_snapshot_archive_entry(
634            &["accounts", "232323"],
635            tar::EntryType::Regular
636        ));
637        assert!(!is_valid_snapshot_archive_entry(
638            &["accounts", "৬.¾"],
639            tar::EntryType::Regular
640        ));
641    }
642
643    #[test]
644    fn test_archive_is_valid_archive_entry() {
645        let path = Path::new("");
646        assert_eq!(
647            is_valid_genesis_archive_entry(path, &["genesis.bin"], tar::EntryType::Regular),
648            UnpackPath::Valid(path)
649        );
650        assert_eq!(
651            is_valid_genesis_archive_entry(path, &["genesis.bin"], tar::EntryType::GNUSparse,),
652            UnpackPath::Valid(path)
653        );
654        assert_eq!(
655            is_valid_genesis_archive_entry(path, &["rocksdb"], tar::EntryType::Directory),
656            UnpackPath::Ignore
657        );
658        assert_eq!(
659            is_valid_genesis_archive_entry(path, &["rocksdb", "foo"], tar::EntryType::Regular),
660            UnpackPath::Ignore
661        );
662        assert_eq!(
663            is_valid_genesis_archive_entry(path, &["rocksdb", "foo"], tar::EntryType::GNUSparse,),
664            UnpackPath::Ignore
665        );
666        assert_eq!(
667            is_valid_genesis_archive_entry(path, &["rocksdb_fifo"], tar::EntryType::Directory),
668            UnpackPath::Ignore
669        );
670        assert_eq!(
671            is_valid_genesis_archive_entry(path, &["rocksdb_fifo", "foo"], tar::EntryType::Regular),
672            UnpackPath::Ignore
673        );
674        assert_eq!(
675            is_valid_genesis_archive_entry(
676                path,
677                &["rocksdb_fifo", "foo"],
678                tar::EntryType::GNUSparse,
679            ),
680            UnpackPath::Ignore
681        );
682        assert_eq!(
683            is_valid_genesis_archive_entry(path, &["aaaa"], tar::EntryType::Regular),
684            UnpackPath::Invalid
685        );
686        assert_eq!(
687            is_valid_genesis_archive_entry(path, &["aaaa"], tar::EntryType::GNUSparse,),
688            UnpackPath::Invalid
689        );
690        assert_eq!(
691            is_valid_genesis_archive_entry(path, &["rocksdb"], tar::EntryType::Regular),
692            UnpackPath::Invalid
693        );
694        assert_eq!(
695            is_valid_genesis_archive_entry(path, &["rocksdb"], tar::EntryType::GNUSparse,),
696            UnpackPath::Invalid
697        );
698        assert_eq!(
699            is_valid_genesis_archive_entry(path, &["rocksdb", "foo"], tar::EntryType::Directory,),
700            UnpackPath::Invalid
701        );
702        assert_eq!(
703            is_valid_genesis_archive_entry(
704                path,
705                &["rocksdb", "foo", "bar"],
706                tar::EntryType::Directory,
707            ),
708            UnpackPath::Invalid
709        );
710        assert_eq!(
711            is_valid_genesis_archive_entry(
712                path,
713                &["rocksdb", "foo", "bar"],
714                tar::EntryType::Regular
715            ),
716            UnpackPath::Invalid
717        );
718        assert_eq!(
719            is_valid_genesis_archive_entry(
720                path,
721                &["rocksdb", "foo", "bar"],
722                tar::EntryType::GNUSparse
723            ),
724            UnpackPath::Invalid
725        );
726        assert_eq!(
727            is_valid_genesis_archive_entry(path, &["rocksdb_fifo"], tar::EntryType::Regular),
728            UnpackPath::Invalid
729        );
730        assert_eq!(
731            is_valid_genesis_archive_entry(path, &["rocksdb_fifo"], tar::EntryType::GNUSparse,),
732            UnpackPath::Invalid
733        );
734        assert_eq!(
735            is_valid_genesis_archive_entry(
736                path,
737                &["rocksdb_fifo", "foo"],
738                tar::EntryType::Directory,
739            ),
740            UnpackPath::Invalid
741        );
742        assert_eq!(
743            is_valid_genesis_archive_entry(
744                path,
745                &["rocksdb_fifo", "foo", "bar"],
746                tar::EntryType::Directory,
747            ),
748            UnpackPath::Invalid
749        );
750        assert_eq!(
751            is_valid_genesis_archive_entry(
752                path,
753                &["rocksdb_fifo", "foo", "bar"],
754                tar::EntryType::Regular
755            ),
756            UnpackPath::Invalid
757        );
758        assert_eq!(
759            is_valid_genesis_archive_entry(
760                path,
761                &["rocksdb_fifo", "foo", "bar"],
762                tar::EntryType::GNUSparse
763            ),
764            UnpackPath::Invalid
765        );
766    }
767
768    fn with_finalize_and_unpack<C>(archive: tar::Builder<Vec<u8>>, checker: C) -> Result<()>
769    where
770        C: Fn(&mut Archive<BufReader<&[u8]>>, &Path) -> Result<()>,
771    {
772        let data = archive.into_inner().unwrap();
773        let reader = BufReader::new(&data[..]);
774        let mut archive: Archive<std::io::BufReader<&[u8]>> = Archive::new(reader);
775        let temp_dir = tempfile::TempDir::new().unwrap();
776
777        checker(&mut archive, temp_dir.path())?;
778        // Check that there is no bad permissions preventing deletion.
779        let result = temp_dir.close();
780        assert_matches!(result, Ok(()));
781        Ok(())
782    }
783
784    fn finalize_and_unpack_snapshot(archive: tar::Builder<Vec<u8>>) -> Result<()> {
785        with_finalize_and_unpack(archive, |a, b| {
786            unpack_snapshot_with_processors(a, b, &[PathBuf::new()], None, |_, _| {}, |_| {})
787        })
788    }
789
790    fn finalize_and_unpack_genesis(archive: tar::Builder<Vec<u8>>) -> Result<()> {
791        with_finalize_and_unpack(archive, |a, b| {
792            unpack_genesis(a, b, MAX_GENESIS_ARCHIVE_UNPACKED_SIZE)
793        })
794    }
795
796    #[test]
797    fn test_archive_unpack_snapshot_ok() {
798        let mut header = Header::new_gnu();
799        header.set_path("version").unwrap();
800        header.set_size(4);
801        header.set_cksum();
802
803        let data: &[u8] = &[1, 2, 3, 4];
804
805        let mut archive = Builder::new(Vec::new());
806        archive.append(&header, data).unwrap();
807
808        let result = finalize_and_unpack_snapshot(archive);
809        assert_matches!(result, Ok(()));
810    }
811
812    #[test]
813    fn test_archive_unpack_genesis_ok() {
814        let mut header = Header::new_gnu();
815        header.set_path("genesis.bin").unwrap();
816        header.set_size(4);
817        header.set_cksum();
818
819        let data: &[u8] = &[1, 2, 3, 4];
820
821        let mut archive = Builder::new(Vec::new());
822        archive.append(&header, data).unwrap();
823
824        let result = finalize_and_unpack_genesis(archive);
825        assert_matches!(result, Ok(()));
826    }
827
828    #[test]
829    fn test_archive_unpack_genesis_bad_perms() {
830        let mut archive = Builder::new(Vec::new());
831
832        let mut header = Header::new_gnu();
833        header.set_path("rocksdb").unwrap();
834        header.set_entry_type(Directory);
835        header.set_size(0);
836        header.set_cksum();
837        let data: &[u8] = &[];
838        archive.append(&header, data).unwrap();
839
840        let mut header = Header::new_gnu();
841        header.set_path("rocksdb/test").unwrap();
842        header.set_size(4);
843        header.set_cksum();
844        let data: &[u8] = &[1, 2, 3, 4];
845        archive.append(&header, data).unwrap();
846
847        // Removing all permissions makes it harder to delete this directory
848        // or work with files inside it.
849        let mut header = Header::new_gnu();
850        header.set_path("rocksdb").unwrap();
851        header.set_entry_type(Directory);
852        header.set_mode(0o000);
853        header.set_size(0);
854        header.set_cksum();
855        let data: &[u8] = &[];
856        archive.append(&header, data).unwrap();
857
858        let result = finalize_and_unpack_genesis(archive);
859        assert_matches!(result, Ok(()));
860    }
861
862    #[test]
863    fn test_archive_unpack_genesis_bad_rocksdb_subdir() {
864        let mut archive = Builder::new(Vec::new());
865
866        let mut header = Header::new_gnu();
867        header.set_path("rocksdb").unwrap();
868        header.set_entry_type(Directory);
869        header.set_size(0);
870        header.set_cksum();
871        let data: &[u8] = &[];
872        archive.append(&header, data).unwrap();
873
874        // tar-rs treats following entry as a Directory to support old tar formats.
875        let mut header = Header::new_gnu();
876        header.set_path("rocksdb/test/").unwrap();
877        header.set_entry_type(Regular);
878        header.set_size(0);
879        header.set_cksum();
880        let data: &[u8] = &[];
881        archive.append(&header, data).unwrap();
882
883        let result = finalize_and_unpack_genesis(archive);
884        assert_matches!(result, Err(UnpackError::Archive(ref message)) if message == "invalid path found: \"rocksdb/test/\"");
885    }
886
887    #[test]
888    fn test_archive_unpack_snapshot_invalid_path() {
889        let mut header = Header::new_gnu();
890        // bypass the sanitization of the .set_path()
891        for (p, c) in header
892            .as_old_mut()
893            .name
894            .iter_mut()
895            .zip(b"foo/../../../dangerous".iter().chain(Some(&0)))
896        {
897            *p = *c;
898        }
899        header.set_size(4);
900        header.set_cksum();
901
902        let data: &[u8] = &[1, 2, 3, 4];
903
904        let mut archive = Builder::new(Vec::new());
905        archive.append(&header, data).unwrap();
906        let result = finalize_and_unpack_snapshot(archive);
907        assert_matches!(result, Err(UnpackError::Archive(ref message)) if message == "invalid path found: \"foo/../../../dangerous\"");
908    }
909
910    fn with_archive_unpack_snapshot_invalid_path(path: &str) -> Result<()> {
911        let mut header = Header::new_gnu();
912        // bypass the sanitization of the .set_path()
913        for (p, c) in header
914            .as_old_mut()
915            .name
916            .iter_mut()
917            .zip(path.as_bytes().iter().chain(Some(&0)))
918        {
919            *p = *c;
920        }
921        header.set_size(4);
922        header.set_cksum();
923
924        let data: &[u8] = &[1, 2, 3, 4];
925
926        let mut archive = Builder::new(Vec::new());
927        archive.append(&header, data).unwrap();
928        with_finalize_and_unpack(archive, |unpacking_archive, path| {
929            for entry in unpacking_archive.entries()? {
930                if !entry?.unpack_in(path)? {
931                    return Err(UnpackError::Archive("failed!".to_string()));
932                } else if !path.join(path).exists() {
933                    return Err(UnpackError::Archive("not existing!".to_string()));
934                }
935            }
936            Ok(())
937        })
938    }
939
940    #[test]
941    fn test_archive_unpack_itself() {
942        assert_matches!(
943            with_archive_unpack_snapshot_invalid_path("ryoqun/work"),
944            Ok(())
945        );
946        // Absolute paths are neutralized as relative
947        assert_matches!(
948            with_archive_unpack_snapshot_invalid_path("/etc/passwd"),
949            Ok(())
950        );
951        assert_matches!(with_archive_unpack_snapshot_invalid_path("../../../dangerous"), Err(UnpackError::Archive(ref message)) if message == "failed!");
952    }
953
954    #[test]
955    fn test_archive_unpack_snapshot_invalid_entry() {
956        let mut header = Header::new_gnu();
957        header.set_path("foo").unwrap();
958        header.set_size(4);
959        header.set_cksum();
960
961        let data: &[u8] = &[1, 2, 3, 4];
962
963        let mut archive = Builder::new(Vec::new());
964        archive.append(&header, data).unwrap();
965        let result = finalize_and_unpack_snapshot(archive);
966        assert_matches!(result, Err(UnpackError::Archive(ref message)) if message == "extra entry found: \"foo\" Regular");
967    }
968
969    #[test]
970    fn test_archive_unpack_snapshot_too_large() {
971        let mut header = Header::new_gnu();
972        header.set_path("version").unwrap();
973        header.set_size(1024 * 1024 * 1024 * 1024 * 1024);
974        header.set_cksum();
975
976        let data: &[u8] = &[1, 2, 3, 4];
977
978        let mut archive = Builder::new(Vec::new());
979        archive.append(&header, data).unwrap();
980        let result = finalize_and_unpack_snapshot(archive);
981        assert_matches!(
982            result,
983            Err(UnpackError::Archive(ref message))
984                if message == &format!(
985                    "too large archive: 1125899906842624 than limit: {}", MAX_SNAPSHOT_ARCHIVE_UNPACKED_APPARENT_SIZE
986                )
987        );
988    }
989
990    #[test]
991    fn test_archive_unpack_snapshot_bad_unpack() {
992        let result = check_unpack_result(false, "abc".to_string());
993        assert_matches!(result, Err(UnpackError::Archive(ref message)) if message == "failed to unpack: \"abc\"");
994    }
995
996    #[test]
997    fn test_archive_checked_total_size_sum() {
998        let result = checked_total_size_sum(500, 500, MAX_SNAPSHOT_ARCHIVE_UNPACKED_ACTUAL_SIZE);
999        assert_matches!(result, Ok(1000));
1000
1001        let result = checked_total_size_sum(
1002            u64::max_value() - 2,
1003            2,
1004            MAX_SNAPSHOT_ARCHIVE_UNPACKED_ACTUAL_SIZE,
1005        );
1006        assert_matches!(
1007            result,
1008            Err(UnpackError::Archive(ref message))
1009                if message == &format!(
1010                    "too large archive: 18446744073709551615 than limit: {}", MAX_SNAPSHOT_ARCHIVE_UNPACKED_ACTUAL_SIZE
1011                )
1012        );
1013    }
1014
1015    #[test]
1016    fn test_archive_checked_total_size_count() {
1017        let result = checked_total_count_increment(101, MAX_SNAPSHOT_ARCHIVE_UNPACKED_COUNT);
1018        assert_matches!(result, Ok(102));
1019
1020        let result =
1021            checked_total_count_increment(999_999_999_999, MAX_SNAPSHOT_ARCHIVE_UNPACKED_COUNT);
1022        assert_matches!(
1023            result,
1024            Err(UnpackError::Archive(ref message))
1025                if message == "too many files in snapshot: 1000000000000"
1026        );
1027    }
1028}