Skip to content

Commit

Permalink
entry: convert hard link absolute paths to relative ones
Browse files Browse the repository at this point in the history
When unpacking a docker image, some hard links may be pointing to, say,
/bin/gunzip. Those were failing the validate_inside_dst() check,
preventing to unpack the image.

Fix this by re-using the same logic as we are already using for actual
files, removing the leading '/' from the link target path.

We do this only if the absolute link is not an ancestor of the target
dir as we want to support this use case according to the existing
absolute_hardlink() test.
  • Loading branch information
Guillaume Desmottes committed May 28, 2021
1 parent 065368b commit a36d2de
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 2 deletions.
33 changes: 33 additions & 0 deletions src/entry.rs
Expand Up @@ -433,6 +433,21 @@ impl<'a> EntryFields<'a> {
})
}

// on Windows one path may be using the extended length syntax while another
// is not, so we remove any prefix before comparing them.
fn path_has_base(&self, path: &Path, base: &Path) -> bool {
let path: PathBuf = path
.components()
.skip_while(|c| matches!(*c, Component::Prefix(_)))
.collect();
let base: PathBuf = base
.components()
.skip_while(|c| matches!(*c, Component::Prefix(_)))
.collect();

path.starts_with(&base)
}

/// Returns access to the header of this entry in the archive.
fn unpack(&mut self, target_base: Option<&Path>, dst: &Path) -> io::Result<Unpacked> {
let kind = self.header.entry_type();
Expand Down Expand Up @@ -467,6 +482,9 @@ impl<'a> EntryFields<'a> {
// the destination of this hard link is both present and
// inside our own directory. This is needed because we want
// to make sure to not overwrite anything outside the root.
// If the link points to an absolute path, assume it's relative
// to the target dir by removing the leading '/', as we already
// do for files, see unpack_in() comments.
//
// Note that this logic is only needed for hard links
// currently. With symlinks the `validate_inside_dst` which
Expand All @@ -475,6 +493,21 @@ impl<'a> EntryFields<'a> {
// links though they're canonicalized to their existing path
// so we need to validate at this time.
Some(ref p) => {
let mut src = src.to_path_buf();
if src.is_absolute() {
let dest_canon = p.canonicalize()?;
if !self.path_has_base(&src, &dest_canon) {
// Skip root component, making the target relative to the target dir.
// Also skip prefix because on Windows the path may use the extended syntax.
src = src
.components()
.skip_while(|c| {
matches!(*c, Component::RootDir | Component::Prefix(_))
})
.collect();
}
}

let link_src = p.join(src);
self.validate_inside_dst(p, &link_src)?;
link_src
Expand Down
28 changes: 26 additions & 2 deletions tests/entry.rs
Expand Up @@ -44,6 +44,16 @@ fn absolute_symlink() {
#[test]
fn absolute_hardlink() {
let td = t!(Builder::new().prefix("tar").tempdir());
// on macos the tempdir is created in /var/folders/ which is actually
// /private/var/folders/. Canonicalize the path so the link target has
// the proper base. Do not do this on Windows as canonicalize() will
// convert the path to the extended length syntax which cannot be used
// in the tarball paths.
let td_canon = if cfg!(unix) {
td.path().canonicalize().unwrap()
} else {
td.path().to_path_buf()
};
let mut ar = tar::Builder::new(Vec::new());

let mut header = tar::Header::new_gnu();
Expand All @@ -58,16 +68,30 @@ fn absolute_hardlink() {
header.set_entry_type(tar::EntryType::Link);
t!(header.set_path("bar"));
// This absolute path under tempdir will be created at unpack time
t!(header.set_link_name(td.path().join("foo")));
t!(header.set_link_name(td_canon.join("foo")));
header.set_cksum();
t!(ar.append(&header, &[][..]));

let mut header = tar::Header::new_gnu();
header.set_size(0);
header.set_entry_type(tar::EntryType::Link);
t!(header.set_path("baz"));
// This absolute path on root will be converted when unpacking
if cfg!(unix) {
t!(header.set_link_name("/foo"));
} else {
t!(header.set_link_name("C:\\foo"));
}
header.set_cksum();
t!(ar.append(&header, &[][..]));

let bytes = t!(ar.into_inner());
let mut ar = tar::Archive::new(&bytes[..]);

t!(ar.unpack(td.path()));
t!(ar.unpack(&td_canon));
t!(td.path().join("foo").metadata());
t!(td.path().join("bar").metadata());
t!(td.path().join("baz").metadata());
}

#[test]
Expand Down

0 comments on commit a36d2de

Please sign in to comment.