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