From 3822b47b266d595c34ea28ce7dc2acdce245751a Mon Sep 17 00:00:00 2001 From: Erik Schamper <1254028+Schamper@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:39:43 +0200 Subject: [PATCH] Fix relative symlinks within a mounted filesystem (#832) --- dissect/target/filesystem.py | 11 +++- dissect/target/filesystems/itunes.py | 2 +- dissect/target/filesystems/tar.py | 2 +- dissect/target/helpers/fsutil.py | 19 ++++-- tests/test_filesystem.py | 89 ++++++++++++++++++---------- 5 files changed, 83 insertions(+), 40 deletions(-) diff --git a/dissect/target/filesystem.py b/dissect/target/filesystem.py index ba6435b0c..e22c099ec 100644 --- a/dissect/target/filesystem.py +++ b/dissect/target/filesystem.py @@ -753,7 +753,7 @@ def readlink_ext(self) -> FilesystemEntry: """ log.debug("%r::readlink_ext()", self) # Default behavior, resolve link own filesystem. - return fsutil.resolve_link(fs=self.fs, entry=self) + return fsutil.resolve_link(self.fs, self.readlink(), self.path, alt_separator=self.fs.alt_separator) def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result: """Determine the stat information of this entry. @@ -1467,10 +1467,15 @@ def _get_from_entry(self, path: str, entry: FilesystemEntry) -> FilesystemEntry: """Get a :class:`FilesystemEntry` relative to a specific entry.""" parts = path.split("/") - for part in parts: + for i, part in enumerate(parts): if entry.is_symlink(): # Resolve using the RootFilesystem instead of the entry's Filesystem - entry = fsutil.resolve_link(fs=self, entry=entry) + entry = fsutil.resolve_link( + self, + entry.readlink(), + "/".join(parts[:i]), + alt_separator=entry.fs.alt_separator, + ) entry = entry.get(part) return entry diff --git a/dissect/target/filesystems/itunes.py b/dissect/target/filesystems/itunes.py index 56b1d315d..6831d09c4 100644 --- a/dissect/target/filesystems/itunes.py +++ b/dissect/target/filesystems/itunes.py @@ -94,7 +94,7 @@ def readlink(self) -> str: def readlink_ext(self) -> FilesystemEntry: """Read the link if this entry is a symlink. Returns a filesystem entry.""" # Can't use the one in VirtualFile as it overrides the FilesystemEntry - return fsutil.resolve_link(fs=self.fs, entry=self) + return fsutil.resolve_link(self.fs, self.readlink(), self.path, alt_separator=self.fs.alt_separator) def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result: """Return the stat information of this entry.""" diff --git a/dissect/target/filesystems/tar.py b/dissect/target/filesystems/tar.py index aadb2b993..0ea9684de 100644 --- a/dissect/target/filesystems/tar.py +++ b/dissect/target/filesystems/tar.py @@ -121,7 +121,7 @@ def readlink(self) -> str: def readlink_ext(self) -> FilesystemEntry: """Read the link if this entry is a symlink. Returns a filesystem entry.""" # Can't use the one in VirtualFile as it overrides the FilesystemEntry - return fsutil.resolve_link(fs=self.fs, entry=self) + return fsutil.resolve_link(self.fs, self.readlink(), self.path, alt_separator=self.fs.alt_separator) def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result: """Return the stat information of this entry.""" diff --git a/dissect/target/helpers/fsutil.py b/dissect/target/helpers/fsutil.py index efb4a1e36..1b1eec9b4 100644 --- a/dissect/target/helpers/fsutil.py +++ b/dissect/target/helpers/fsutil.py @@ -440,15 +440,20 @@ def has_glob_magic(s) -> bool: def resolve_link( - fs: filesystem.Filesystem, entry: filesystem.FilesystemEntry, previous_links: set[str] = None + fs: filesystem.Filesystem, + link: str, + path: str, + *, + alt_separator: str = "", + previous_links: set[str] | None = None, ) -> filesystem.FilesystemEntry: """Resolves a symlink to its actual path. It stops resolving once it detects an infinite recursion loop. """ - link = normalize(entry.readlink(), alt_separator=entry.fs.alt_separator) - path = normalize(entry.path, alt_separator=entry.fs.alt_separator) + link = normalize(link, alt_separator=alt_separator) + path = normalize(path, alt_separator=alt_separator) # Create hash for entry based on path and link link_id = f"{path}{link}" @@ -471,7 +476,13 @@ def resolve_link( entry = fs.get(link) if entry.is_symlink(): - entry = resolve_link(fs, entry, previous_links) + entry = resolve_link( + fs, + entry.readlink(), + link, + alt_separator=entry.fs.alt_separator, + previous_links=previous_links, + ) return entry diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index 97aa3f79f..fa241245d 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -12,7 +12,7 @@ import pytest from dissect.util import ts -from dissect.target import Target, filesystem +from dissect.target import filesystem from dissect.target.exceptions import ( FileNotFoundError, NotADirectoryError, @@ -91,7 +91,9 @@ def test_get(vfs: VirtualFilesystem) -> None: assert vfs.get("filelink2").stat() == vfs.get("/path/to/some/file").stat() -def test_symlink_across_layers(target_bare: Target) -> None: +def test_symlink_across_layers() -> None: + lfs = LayerFilesystem() + vfs1 = VirtualFilesystem() vfs1.makedirs("/path/to/symlink/") vfs1.symlink("../target", "/path/to/symlink/target") @@ -99,18 +101,20 @@ def test_symlink_across_layers(target_bare: Target) -> None: vfs2 = VirtualFilesystem() target_dir = vfs2.makedirs("/path/to/target") - layer1 = target_bare.fs.append_layer() + layer1 = lfs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.append_layer() + layer2 = lfs.append_layer() layer2.mount("/", vfs2) - target_entry = target_bare.fs.get("/path/to/symlink/target").readlink_ext() + target_entry = lfs.get("/path/to/symlink/target").readlink_ext() assert target_dir.stat() == target_entry.entries[0].stat() -def test_symlink_files_across_layers(target_bare: Target) -> None: +def test_symlink_files_across_layers() -> None: + lfs = LayerFilesystem() + vfs1 = VirtualFilesystem() vfs1.makedirs("/path/to/symlink/") vfs1.symlink("../target", "/path/to/symlink/target") @@ -118,19 +122,21 @@ def test_symlink_files_across_layers(target_bare: Target) -> None: vfs2 = VirtualFilesystem() target_dir = vfs2.makedirs("/path/to/target/derp") - layer1 = target_bare.fs.append_layer() + layer1 = lfs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.append_layer() + layer2 = lfs.append_layer() layer2.mount("/", vfs2) - target_entry = target_bare.fs.get("/path/to/symlink/target/derp") + target_entry = lfs.get("/path/to/symlink/target/derp") assert len(target_entry.entries) != 0 assert target_dir.stat() == target_entry.stat() -def test_symlink_to_symlink_across_layers(target_bare: Target) -> None: +def test_symlink_to_symlink_across_layers() -> None: + lfs = LayerFilesystem() + vfs1 = VirtualFilesystem() vfs1.makedirs("/path/to/symlink/") target_dir = vfs1.makedirs("/path/target") @@ -139,18 +145,20 @@ def test_symlink_to_symlink_across_layers(target_bare: Target) -> None: vfs2 = VirtualFilesystem() vfs2.symlink("../target", "/path/to/target") - layer1 = target_bare.fs.append_layer() + layer1 = lfs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.append_layer() + layer2 = lfs.append_layer() layer2.mount("/", vfs2) - target_entry = target_bare.fs.get("/path/to/symlink/target/").readlink_ext() + target_entry = lfs.get("/path/to/symlink/target/").readlink_ext() assert target_dir.stat() == target_entry.stat() -def test_recursive_symlink_across_layers(target_bare: Target) -> None: +def test_recursive_symlink_across_layers() -> None: + lfs = LayerFilesystem() + vfs1 = VirtualFilesystem() vfs1.makedirs("/path/to/symlink/") vfs1.symlink("../target", "/path/to/symlink/target") @@ -158,17 +166,19 @@ def test_recursive_symlink_across_layers(target_bare: Target) -> None: vfs2 = VirtualFilesystem() vfs2.symlink("symlink/target", "/path/to/target") - layer1 = target_bare.fs.append_layer() + layer1 = lfs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.append_layer() + layer2 = lfs.append_layer() layer2.mount("/", vfs2) with pytest.raises(SymlinkRecursionError): - target_bare.fs.get("/path/to/symlink/target/").readlink_ext() + lfs.get("/path/to/symlink/target/").readlink_ext() -def test_symlink_across_3_layers(target_bare: Target) -> None: +def test_symlink_across_3_layers() -> None: + lfs = LayerFilesystem() + vfs1 = VirtualFilesystem() vfs1.makedirs("/path/to/symlink/") vfs1.symlink("../target", "/path/to/symlink/target") @@ -179,24 +189,26 @@ def test_symlink_across_3_layers(target_bare: Target) -> None: vfs3 = VirtualFilesystem() target_dir = vfs3.makedirs("/path/target") - layer1 = target_bare.fs.append_layer() + layer1 = lfs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.append_layer() + layer2 = lfs.append_layer() layer2.mount("/", vfs2) - layer3 = target_bare.fs.append_layer() + layer3 = lfs.append_layer() layer3.mount("/", vfs3) - target_entry = target_bare.fs.get("/path/to/symlink/target/").readlink_ext() + target_entry = lfs.get("/path/to/symlink/target/").readlink_ext() assert target_dir.stat() == target_entry.stat() - stat_b = target_bare.fs.get("/path/to/symlink/target/").stat() - stat_a = target_bare.fs.get("/path/to/target/").stat() + stat_b = lfs.get("/path/to/symlink/target/").stat() + stat_a = lfs.get("/path/to/target/").stat() assert stat_a == stat_b -def test_recursive_symlink_open_across_layers(target_bare: Target) -> None: +def test_recursive_symlink_open_across_layers() -> None: + lfs = LayerFilesystem() + vfs1 = VirtualFilesystem() vfs1.makedirs("/path/to/symlink/") vfs1.symlink("../target", "/path/to/symlink/target") @@ -204,22 +216,24 @@ def test_recursive_symlink_open_across_layers(target_bare: Target) -> None: vfs2 = VirtualFilesystem() vfs2.symlink("symlink/target", "/path/to/target") - layer1 = target_bare.fs.append_layer() + layer1 = lfs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.append_layer() + layer2 = lfs.append_layer() layer2.mount("/", vfs2) with pytest.raises(SymlinkRecursionError): - target_bare.fs.get("/path/to/symlink/target/").open() + lfs.get("/path/to/symlink/target/").open() + +def test_recursive_symlink_dev() -> None: + lfs = LayerFilesystem() -def test_recursive_symlink_dev(target_bare: Target) -> None: fs1 = ExtFilesystem(fh=open(absolute_path("_data/filesystems/symlink_disk.ext4"), "rb")) - target_bare.fs.mount(fs=fs1, path="/") + lfs.mount(fs=fs1, path="/") with pytest.raises(SymlinkRecursionError): - target_bare.fs.get("/path/to/symlink/target/").readlink_ext() + lfs.get("/path/to/symlink/target/").readlink_ext() @pytest.mark.parametrize( @@ -1224,3 +1238,16 @@ def test_layer_filesystem_mount() -> None: assert sorted(lfs.listdir("/vfs")) == ["file1", "file2"] assert lfs.path("/vfs/file1").read_text() == "value1" assert lfs.path("/vfs/file2").read_text() == "value2" + + +def test_layer_filesystem_relative_link() -> None: + """test relative symlinks from a filesystem mounted at a subdirectory""" + lfs = LayerFilesystem() + + vfs = VirtualFilesystem() + vfs.map_file_fh("dir/hello/world", BytesIO("o/".encode())) + vfs.symlink("dir/hello", "bye") + + lfs.mount("/mnt", vfs) + + assert lfs.path("/mnt/bye/world").read_text() == "o/"