Skip to content

Commit

Permalink
Fix relative symlinks within a mounted filesystem (#832)
Browse files Browse the repository at this point in the history
  • Loading branch information
Schamper authored Aug 26, 2024
1 parent d5c7315 commit 3822b47
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 40 deletions.
11 changes: 8 additions & 3 deletions dissect/target/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dissect/target/filesystems/itunes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
2 changes: 1 addition & 1 deletion dissect/target/filesystems/tar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
19 changes: 15 additions & 4 deletions dissect/target/helpers/fsutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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

Expand Down
89 changes: 58 additions & 31 deletions tests/test_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -91,46 +91,52 @@ 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")

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")

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")
Expand All @@ -139,36 +145,40 @@ 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")

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")
Expand All @@ -179,47 +189,51 @@ 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")

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(
Expand Down Expand Up @@ -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/"

0 comments on commit 3822b47

Please sign in to comment.