From 9008bd004107e3895a531c126b99f8b84d6bdc96 Mon Sep 17 00:00:00 2001 From: Torres Date: Thu, 5 Mar 2026 15:19:39 +0200 Subject: [PATCH 1/2] Added file descriptors iterator to ProcProcess --- dissect/target/plugins/os/unix/linux/proc.py | 57 ++++++++++++++++++++ tests/conftest.py | 9 +++- tests/plugins/os/unix/linux/test_proc.py | 15 ++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/unix/linux/proc.py b/dissect/target/plugins/os/unix/linux/proc.py index 9b1b401b0b..22b5c83e9d 100644 --- a/dissect/target/plugins/os/unix/linux/proc.py +++ b/dissect/target/plugins/os/unix/linux/proc.py @@ -154,6 +154,50 @@ class Environ: variable: str contents: str +@dataclass +class FileDescriptor: + def __init__(self, proc: Path, fd_num: str): + self.number = int(fd_num) + self.path = proc.joinpath(fd_num) + self.info_path = proc.parent.joinpath("fdinfo", fd_num) + self.target = None + self.fd_info = None + + @property + def link(self) -> str: + """Returns the resolved symlink""" + if not self.target is None: + return self.target + try: + self.target = self.path.readlink() + except Exception: + self.target = "unknown" + return self.target + + @property + def info(self) -> dict: + """Parsed key-value pairs from fdinfo""" + if self.fd_info is None: + self.fd_info = self._parse_fdinfo() + return self.fd_info + + def _parse_fdinfo(self): + data = {} + if not self.info_path.exists(): + return data + + for line in self.info_path.read_text().split('\n'): + line = line.strip() + if not line: + continue + + parts = line.split(None, 1) + if not len(parts) == 2: + continue + key, value = parts + data[key] = value + + return data class ProcessStateEnum(StrEnum): R = "Running" # Running @@ -490,6 +534,15 @@ def _parse_environ(self) -> Iterator[Environ]: yield Environ(variable, contents) + def _parse_fd(self) -> Iterator[FileDescriptor]: + """Internal function to parse entries in ``/proc/[pid]/fd/[fd_num]`` and ``/proc/[pid]/fdinfo/[fd_num]``.""" + if not (fd_path := self.get("fd")).exists(): + return + + for fd_link in fd_path.iterdir(): + yield FileDescriptor(fd_path, fd_link.name) + + @property def _boottime(self) -> int | None: """Returns the boot time of the system. @@ -585,6 +638,10 @@ def environ(self) -> Iterator[Environ]: """Yields the content of the environ file associated with the process.""" yield from self._parse_environ() + def fd(self) -> Iterator[FileDescriptor]: + """Yields the file descriptors associated with the process.""" + yield from self._parse_fd() + @property def uptime(self) -> timedelta: """Returns the uptime of the system from the moment it was acquired.""" diff --git a/tests/conftest.py b/tests/conftest.py index fe34e55b57..1947892107 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -239,6 +239,12 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: "", ), ) + fdinfo_files_data = ( + "pos\t0\nflags\t00000002\nsocket\t1337\n", + "pos\t0\nflags\t00000002\nsocket\t1338\n", + "pos\t0\nflags\t00000002\nsocket\t1339\n", + "pos\t0\nflags\t00000002\nsocket\t1337\n" + ) stat_files_data = ( "1 (systemd) S 0 1 1 0 -1 4194560 53787 457726 166 4255 112 260 761 548 20 0 1 0 30 184877056 2658 18446744073709551615 93937510957056 93937511789581 140726499200496 0 0 0 671173123 4096 1260 0 0 0 17 0 0 0 11 0 0 93937512175824 93937512476912 93937519890432 140726499204941 140726499204952 140726499204952 140726499205101 0\n", # noqa "2 (kthread) K 1 1 1 0 -1 4194560 53787 457726 166 4255 112 260 761 548 20 0 1 0 30 184877056 2658 18446744073709551615 93937510957056 93937511789581 140726499200496 0 0 0 671173123 4096 1260 0 0 0 17 0 0 0 11 0 0 93937512175824 93937512476912 93937519890432 140726499204941 140726499204952 140726499204952 140726499205101 0\n", # noqa @@ -251,6 +257,7 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: fs.makedirs(dir) fs.map_file_entry(fd.path, fd) + fs.map_file_fh(dir + "/fdinfo/" + fd.name, BytesIO(fdinfo_files_data[idx].encode())) fs.map_file_fh(dir + "/stat", BytesIO(stat_files_data[idx].encode())) fs.map_file_fh(dir + "/cmdline", BytesIO(cmdline.encode())) if environ: @@ -259,7 +266,7 @@ def fs_linux_proc(fs_linux: VirtualFilesystem) -> VirtualFilesystem: # symlink acquire process to self fs.link("/proc/1337", "/proc/self") - # boottime and uptime are needed for for time tests + # boottime and uptime are needed for time tests fs.map_file_fh("/proc/uptime", BytesIO(b"134368.27 132695.52\n")) fs.map_file_fh("/proc/stat", BytesIO(b"btime 1680559854")) diff --git a/tests/plugins/os/unix/linux/test_proc.py b/tests/plugins/os/unix/linux/test_proc.py index 475aa232a0..ea759b3064 100644 --- a/tests/plugins/os/unix/linux/test_proc.py +++ b/tests/plugins/os/unix/linux/test_proc.py @@ -30,6 +30,11 @@ def test_process(target_linux_users: Target, fs_linux_proc: VirtualFilesystem) - assert environ[0].variable == "VAR" assert environ[0].contents == "1" + fd = list(process.fd()) + assert "socket" in fd[0].link.name + assert fd[0].info["pos"] == "0" + assert fd[0].info["flags"] == "00000002" + def test_process_not_found(target_linux_users: Target, fs_linux_proc: VirtualFilesystem) -> None: target_linux_users.add_plugin(ProcPlugin) @@ -52,6 +57,11 @@ def test_processes(target_linux_users: Target, fs_linux_proc: VirtualFilesystem) assert env.variable == "VAR" assert env.contents == "1" + for fd in process.fd(): + assert "socket" in fd.link.name + assert fd.info["pos"] == "0" + assert fd.info["flags"] == "00000002" + def test_processes_without_boottime(target_linux_users: Target, fs_linux_proc: VirtualFilesystem) -> None: target_linux_users.add_plugin(ProcPlugin) @@ -70,6 +80,11 @@ def test_processes_without_boottime(target_linux_users: Target, fs_linux_proc: V assert env.variable == "VAR" assert env.contents == "1" + for fd in process.fd(): + assert "socket" in fd.link.name + assert fd.info["pos"] == "0" + assert fd.info["flags"] == "00000002" + def test_proc_plugin_incompatible(target_linux_users: Target, fs_linux: VirtualFilesystem) -> None: with pytest.raises(UnsupportedPluginError, match="No /proc directory found"): From 8920bc8fd7495311802beab9df32e94808329ceb Mon Sep 17 00:00:00 2001 From: Torres Date: Sun, 15 Mar 2026 13:02:41 +0200 Subject: [PATCH 2/2] Refactor FileDescriptor to use cached properties and fix linting --- dissect/target/plugins/os/unix/linux/proc.py | 42 ++++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/dissect/target/plugins/os/unix/linux/proc.py b/dissect/target/plugins/os/unix/linux/proc.py index 604ace265a..818f7b5999 100644 --- a/dissect/target/plugins/os/unix/linux/proc.py +++ b/dissect/target/plugins/os/unix/linux/proc.py @@ -154,45 +154,43 @@ class Environ: variable: str contents: str -@dataclass class FileDescriptor: def __init__(self, proc: Path, fd_num: str): - self.number = int(fd_num) - self.path = proc.joinpath(fd_num) - self.info_path = proc.parent.joinpath("fdinfo", fd_num) - self.target = None - self.fd_info = None + self.proc = proc + self.number = fd_num + + @property + def path(self) -> Path: + """The path to the file descriptor symlink.""" + return self.proc.joinpath(self.number) @property + def info_path(self) -> Path: + """The path to the fdinfo entry.""" + return self.proc.parent.joinpath("fdinfo", self.number) + + @cached_property def link(self) -> str: - """Returns the resolved symlink""" - if not self.target is None: - return self.target + """Returns the resolved symlink.""" try: - self.target = self.path.readlink() + return str(self.path.readlink()) except Exception: - self.target = "unknown" - return self.target - - @property - def info(self) -> dict: - """Parsed key-value pairs from fdinfo""" - if self.fd_info is None: - self.fd_info = self._parse_fdinfo() - return self.fd_info + return "unknown" - def _parse_fdinfo(self): + @cached_property + def info(self)-> dict[str, str]: + """Parsed key-value pairs from fdinfo.""" data = {} if not self.info_path.exists(): return data - for line in self.info_path.read_text().split('\n'): + for line in self.info_path.read_text().split("\n"): line = line.strip() if not line: continue parts = line.split(None, 1) - if not len(parts) == 2: + if len(parts) != 2: continue key, value = parts data[key] = value