mirror of
https://github.com/JakeHillion/drgn.git
synced 2024-12-22 17:23:06 +00:00
tools/fsrefs.py: add mode to find references to a filesystem/super block
In this mode, we print the paths of the referenced files. Now that we have multiple "checks" we're doing, also add an option to enable or disable specific checks. Signed-off-by: Omar Sandoval <osandov@osandov.com>
This commit is contained in:
parent
f8851c54f0
commit
bb137f1887
@ -348,6 +348,45 @@ def mlock(addr, len):
|
||||
_check_ctypes_syscall(_mlock(addr, len))
|
||||
|
||||
|
||||
CSIGNAL = 0x000000FF
|
||||
CLONE_VM = 0x00000100
|
||||
CLONE_FS = 0x00000200
|
||||
CLONE_FILES = 0x00000400
|
||||
CLONE_SIGHAND = 0x00000800
|
||||
CLONE_PIDFD = 0x00001000
|
||||
CLONE_PTRACE = 0x00002000
|
||||
CLONE_VFORK = 0x00004000
|
||||
CLONE_PARENT = 0x00008000
|
||||
CLONE_THREAD = 0x00010000
|
||||
CLONE_NEWNS = 0x00020000
|
||||
CLONE_SYSVSEM = 0x00040000
|
||||
CLONE_SETTLS = 0x00080000
|
||||
CLONE_PARENT_SETTID = 0x00100000
|
||||
CLONE_CHILD_CLEARTID = 0x00200000
|
||||
CLONE_DETACHED = 0x00400000
|
||||
CLONE_UNTRACED = 0x00800000
|
||||
CLONE_CHILD_SETTID = 0x01000000
|
||||
CLONE_NEWCGROUP = 0x02000000
|
||||
CLONE_NEWUTS = 0x04000000
|
||||
CLONE_NEWIPC = 0x08000000
|
||||
CLONE_NEWUSER = 0x10000000
|
||||
CLONE_NEWPID = 0x20000000
|
||||
CLONE_NEWNET = 0x40000000
|
||||
CLONE_IO = 0x80000000
|
||||
CLONE_CLEAR_SIGHAND = 0x100000000
|
||||
CLONE_INTO_CGROUP = 0x200000000
|
||||
CLONE_NEWTIME = 0x00000080
|
||||
|
||||
|
||||
_unshare = _c.unshare
|
||||
_unshare.argtypes = [ctypes.c_int]
|
||||
_unshare.restype = ctypes.c_int
|
||||
|
||||
|
||||
def unshare(flags):
|
||||
_check_ctypes_syscall(_unshare(flags))
|
||||
|
||||
|
||||
_syscall = _c.syscall
|
||||
_syscall.restype = ctypes.c_long
|
||||
|
||||
|
@ -7,11 +7,20 @@ import io
|
||||
import mmap
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from drgn.helpers.linux.fs import fget
|
||||
from drgn.helpers.linux.pid import find_task
|
||||
from tests.linux_kernel import LinuxKernelTestCase, fork_and_sigwait
|
||||
from tests.linux_kernel import (
|
||||
CLONE_NEWNS,
|
||||
LinuxKernelTestCase,
|
||||
fork_and_sigwait,
|
||||
mount,
|
||||
umount,
|
||||
unshare,
|
||||
)
|
||||
from tools.fsrefs import main
|
||||
|
||||
|
||||
@ -41,7 +50,7 @@ class TestFsRefs(LinuxKernelTestCase):
|
||||
fd = os.open(path, os.O_CREAT | os.O_WRONLY, 0o600)
|
||||
try:
|
||||
self.assertRegex(
|
||||
self.run_and_capture("--inode", str(path)),
|
||||
self.run_and_capture("--check", "tasks", "--inode", str(path)),
|
||||
rf"pid {os.getpid()} \(.*\) fd {fd} ",
|
||||
)
|
||||
finally:
|
||||
@ -57,7 +66,7 @@ class TestFsRefs(LinuxKernelTestCase):
|
||||
try:
|
||||
link_fd = os.open(link, os.O_PATH | os.O_NOFOLLOW)
|
||||
try:
|
||||
output = self.run_and_capture("--inode", str(link))
|
||||
output = self.run_and_capture("--check", "tasks", "--inode", str(link))
|
||||
self.assertNotRegex(
|
||||
output,
|
||||
rf"pid {os.getpid()} \(.*\) fd {file_fd} ",
|
||||
@ -67,7 +76,9 @@ class TestFsRefs(LinuxKernelTestCase):
|
||||
rf"pid {os.getpid()} \(.*\) fd {link_fd} ",
|
||||
)
|
||||
|
||||
output = self.run_and_capture("--inode", str(link), "--dereference")
|
||||
output = self.run_and_capture(
|
||||
"--check", "tasks", "--inode", str(link), "--dereference"
|
||||
)
|
||||
self.assertRegex(
|
||||
output,
|
||||
rf"pid {os.getpid()} \(.*\) fd {file_fd} ",
|
||||
@ -89,7 +100,7 @@ class TestFsRefs(LinuxKernelTestCase):
|
||||
path = self._tmp / "dir"
|
||||
with fork_and_sigwait(mkdir_and_chdir, path, 0o600) as pid:
|
||||
self.assertRegex(
|
||||
self.run_and_capture("--inode", str(path)),
|
||||
self.run_and_capture("--check", "tasks", "--inode", str(path)),
|
||||
rf"pid {pid} \(.*\) cwd ",
|
||||
)
|
||||
|
||||
@ -101,13 +112,15 @@ class TestFsRefs(LinuxKernelTestCase):
|
||||
path = self._tmp / "dir"
|
||||
with fork_and_sigwait(mkdir_and_chroot, path, 0o600) as pid:
|
||||
self.assertRegex(
|
||||
self.run_and_capture("--inode", str(path)),
|
||||
self.run_and_capture("--check", "tasks", "--inode", str(path)),
|
||||
rf"pid {pid} \(.*\) root ",
|
||||
)
|
||||
|
||||
def test_exe(self):
|
||||
self.assertRegex(
|
||||
self.run_and_capture("--inode", sys.executable, "--dereference"),
|
||||
self.run_and_capture(
|
||||
"--check", "tasks", "--inode", sys.executable, "--dereference"
|
||||
),
|
||||
rf"pid {os.getpid()} \(.*\) exe ",
|
||||
)
|
||||
|
||||
@ -121,16 +134,65 @@ class TestFsRefs(LinuxKernelTestCase):
|
||||
start = ctypes.addressof(ctypes.c_char.from_buffer(map))
|
||||
end = start + mmap.PAGESIZE
|
||||
self.assertRegex(
|
||||
self.run_and_capture("--inode", str(path)),
|
||||
self.run_and_capture("--check", "tasks", "--inode", str(path)),
|
||||
rf"pid {os.getpid()} \(.*\) vma {hex(start)}-{hex(end)} ",
|
||||
)
|
||||
|
||||
def test_inode_pointer(self):
|
||||
self.assertRegex(
|
||||
self.run_and_capture(
|
||||
"--check",
|
||||
"tasks",
|
||||
"--inode-pointer",
|
||||
hex(find_task(self.prog, os.getpid()).mm.exe_file.f_inode),
|
||||
"--dereference",
|
||||
),
|
||||
rf"pid {os.getpid()} \(.*\) exe ",
|
||||
)
|
||||
|
||||
def test_super_block(self):
|
||||
with contextlib.ExitStack() as exit_stack:
|
||||
mount("tmpfs", self._tmp, "tmpfs")
|
||||
exit_stack.callback(umount, self._tmp)
|
||||
|
||||
pid = exit_stack.enter_context(fork_and_sigwait(unshare, CLONE_NEWNS))
|
||||
|
||||
path1 = self._tmp / "file1"
|
||||
fd1 = os.open(path1, os.O_CREAT | os.O_WRONLY, 0o600)
|
||||
exit_stack.callback(os.close, fd1)
|
||||
|
||||
path2 = self._tmp / "file2"
|
||||
fd2 = os.open(path2, os.O_CREAT | os.O_WRONLY, 0o600)
|
||||
exit_stack.callback(os.close, fd2)
|
||||
|
||||
output = self.run_and_capture(
|
||||
"--check", "mounts", "--check", "tasks", "--super-block", str(self._tmp)
|
||||
)
|
||||
|
||||
with self.subTest("mount"):
|
||||
self.assertIn(f"mount {self._tmp} (struct mount", output)
|
||||
|
||||
with self.subTest("mount in namespace"):
|
||||
ino = Path(f"/proc/{pid}/ns/mnt").stat().st_ino
|
||||
self.assertIn(f"mount {self._tmp} (mount namespace {ino}) ", output)
|
||||
|
||||
with self.subTest("fd"):
|
||||
self.assertRegex(
|
||||
output,
|
||||
rf"pid {os.getpid()} \(.*\) fd {fd1} \(struct file \*\)0x[0-9a-f]+ {re.escape(str(path1))}",
|
||||
)
|
||||
self.assertRegex(
|
||||
output,
|
||||
rf"pid {os.getpid()} \(.*\) fd {fd2} \(struct file \*\)0x[0-9a-f]+ {re.escape(str(path2))}",
|
||||
)
|
||||
|
||||
with self.subTest("super_block_pointer"):
|
||||
self.assertIn(
|
||||
f"mount {self._tmp} ",
|
||||
self.run_and_capture(
|
||||
"--check",
|
||||
"mounts",
|
||||
"--super-block-pointer",
|
||||
hex(fget(find_task(self.prog, os.getpid()), fd1).f_inode.i_sb),
|
||||
),
|
||||
)
|
||||
|
263
tools/fsrefs.py
263
tools/fsrefs.py
@ -7,36 +7,17 @@ import typing
|
||||
from typing import Any, Callable, Optional, Sequence, Union
|
||||
|
||||
from drgn import FaultError, Object, Program
|
||||
from drgn.helpers.linux.fs import fget, for_each_file
|
||||
from drgn.helpers.linux.fs import (
|
||||
d_path,
|
||||
fget,
|
||||
for_each_file,
|
||||
for_each_mount,
|
||||
inode_path,
|
||||
mount_dst,
|
||||
)
|
||||
from drgn.helpers.linux.mm import for_each_vma
|
||||
from drgn.helpers.linux.pid import find_task, for_each_task
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
|
||||
class Visitor(typing.Protocol): # novermin
|
||||
def visit_file(self, file: Object) -> bool:
|
||||
...
|
||||
|
||||
def visit_inode(self, inode: Object) -> bool:
|
||||
...
|
||||
|
||||
def visit_path(self, path: Object) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class InodeVisitor:
|
||||
def __init__(self, inode: Object) -> None:
|
||||
self._inode = inode.read_()
|
||||
|
||||
def visit_file(self, file: Object) -> bool:
|
||||
return file.f_inode == self._inode
|
||||
|
||||
def visit_inode(self, inode: Object) -> bool:
|
||||
return inode == self._inode
|
||||
|
||||
def visit_path(self, path: Object) -> bool:
|
||||
return path.dentry.d_inode == self._inode
|
||||
|
||||
|
||||
class warn_on_fault:
|
||||
def __init__(self, message: Union[str, Callable[[], str]]) -> None:
|
||||
@ -61,13 +42,84 @@ class warn_on_fault:
|
||||
|
||||
ignore_fault = warn_on_fault("")
|
||||
|
||||
|
||||
format_args = {
|
||||
"dereference": False,
|
||||
"symbolize": False,
|
||||
}
|
||||
|
||||
|
||||
def visit_tasks(prog: Program, visitor: "Visitor") -> None:
|
||||
if typing.TYPE_CHECKING:
|
||||
|
||||
class Visitor(typing.Protocol): # novermin
|
||||
def visit_file(self, file: Object) -> Optional[str]:
|
||||
...
|
||||
|
||||
def visit_inode(self, inode: Object) -> Optional[str]:
|
||||
...
|
||||
|
||||
def visit_path(self, path: Object) -> Optional[str]:
|
||||
...
|
||||
|
||||
|
||||
class InodeVisitor:
|
||||
def __init__(self, inode: Object) -> None:
|
||||
self._inode = inode.read_()
|
||||
|
||||
def visit_file(self, file: Object) -> Optional[str]:
|
||||
if file.f_inode != self._inode:
|
||||
return None
|
||||
return file.format_(**format_args)
|
||||
|
||||
def visit_inode(self, inode: Object) -> Optional[str]:
|
||||
if inode != self._inode:
|
||||
return None
|
||||
return inode.format_(**format_args)
|
||||
|
||||
def visit_path(self, path: Object) -> Optional[str]:
|
||||
if path.dentry.d_inode != self._inode:
|
||||
return None
|
||||
return path.format_(**format_args)
|
||||
|
||||
|
||||
class SuperBlockVisitor:
|
||||
def __init__(self, sb: Object) -> None:
|
||||
self._sb = sb.read_()
|
||||
|
||||
def visit_file(self, file: Object) -> Optional[str]:
|
||||
if file.f_inode.i_sb != self._sb:
|
||||
return None
|
||||
match = file.format_(**format_args)
|
||||
with ignore_fault:
|
||||
match += " " + os.fsdecode(d_path(file.f_path))
|
||||
return match
|
||||
|
||||
def visit_inode(self, inode: Object) -> Optional[str]:
|
||||
if inode.i_sb != self._sb:
|
||||
return None
|
||||
match = inode.format_(**format_args)
|
||||
with ignore_fault:
|
||||
path = inode_path(inode)
|
||||
if path:
|
||||
match += " " + os.fsdecode(path)
|
||||
return match
|
||||
|
||||
def visit_path(self, path: Object) -> Optional[str]:
|
||||
if path.mnt.mnt_sb != self._sb:
|
||||
return None
|
||||
match = path.format_(**format_args)
|
||||
with ignore_fault:
|
||||
match += " " + os.fsdecode(d_path(path))
|
||||
return match
|
||||
|
||||
|
||||
def visit_tasks(
|
||||
prog: Program, visitor: "Visitor", *, check_mounts: bool, check_tasks: bool
|
||||
) -> None:
|
||||
check_mounts = check_mounts and isinstance(visitor, SuperBlockVisitor)
|
||||
if check_mounts:
|
||||
init_mnt_ns = prog["init_task"].nsproxy.mnt_ns
|
||||
checked_mnt_ns = {0}
|
||||
with warn_on_fault("iterating tasks"):
|
||||
for task in for_each_task(prog):
|
||||
cached_task_id = None
|
||||
@ -102,38 +154,59 @@ def visit_tasks(prog: Program, visitor: "Visitor") -> None:
|
||||
if mm and mm == group_leader.mm:
|
||||
mm = None
|
||||
|
||||
if files:
|
||||
for fd, file in for_each_file(task):
|
||||
if check_mounts:
|
||||
nsproxy = task.nsproxy.read_()
|
||||
if nsproxy:
|
||||
mnt_ns = nsproxy.mnt_ns.read_()
|
||||
if mnt_ns.value_() not in checked_mnt_ns:
|
||||
for mount in for_each_mount(mnt_ns):
|
||||
with ignore_fault:
|
||||
if mount.mnt.mnt_sb == visitor._sb: # type: ignore [attr-defined]
|
||||
if mnt_ns == init_mnt_ns:
|
||||
mnt_ns_note = ""
|
||||
else:
|
||||
mnt_ns_note = f" (mount namespace {mnt_ns.ns.inum.value_()})"
|
||||
print(
|
||||
f"mount {os.fsdecode(mount_dst(mount))}{mnt_ns_note} "
|
||||
f"{mount.format_(**format_args)}"
|
||||
)
|
||||
|
||||
checked_mnt_ns.add(mnt_ns.value_())
|
||||
|
||||
if check_tasks:
|
||||
if files:
|
||||
for fd, file in for_each_file(task):
|
||||
with ignore_fault:
|
||||
match = visitor.visit_file(file)
|
||||
if match:
|
||||
print(f"{task_id()} fd {fd} {match}")
|
||||
|
||||
if fs:
|
||||
with ignore_fault:
|
||||
if visitor.visit_file(file):
|
||||
print(
|
||||
f"{task_id()} fd {fd} {file.format_(**format_args)}"
|
||||
)
|
||||
|
||||
if fs:
|
||||
with ignore_fault:
|
||||
if visitor.visit_path(fs.root):
|
||||
print(
|
||||
f"{task_id()} root {fs.root.address_of_().format_(**format_args)}"
|
||||
)
|
||||
with ignore_fault:
|
||||
if visitor.visit_path(fs.pwd):
|
||||
print(
|
||||
f"{task_id()} cwd {fs.pwd.address_of_().format_(**format_args)}"
|
||||
)
|
||||
|
||||
if mm:
|
||||
exe_file = mm.exe_file.read_()
|
||||
if exe_file and visitor.visit_file(exe_file):
|
||||
print(f"{task_id()} exe {exe_file.format_(**format_args)}")
|
||||
|
||||
for vma in for_each_vma(mm):
|
||||
match = visitor.visit_path(fs.root.address_of_())
|
||||
if match:
|
||||
print(f"{task_id()} root {match}")
|
||||
with ignore_fault:
|
||||
file = vma.vm_file.read_()
|
||||
if file and visitor.visit_file(file):
|
||||
print(
|
||||
f"{task_id()} vma {hex(vma.vm_start)}-{hex(vma.vm_end)} {vma.format_(**format_args)}"
|
||||
)
|
||||
match = visitor.visit_path(fs.pwd.address_of_())
|
||||
if match:
|
||||
print(f"{task_id()} cwd {match}")
|
||||
|
||||
if mm:
|
||||
exe_file = mm.exe_file.read_()
|
||||
if exe_file:
|
||||
match = visitor.visit_file(exe_file)
|
||||
if match:
|
||||
print(f"{task_id()} exe {match}")
|
||||
|
||||
for vma in for_each_vma(mm):
|
||||
with ignore_fault:
|
||||
file = vma.vm_file.read_()
|
||||
if file:
|
||||
match = visitor.visit_file(file)
|
||||
if match:
|
||||
print(
|
||||
f"{task_id()} vma {hex(vma.vm_start)}-{hex(vma.vm_end)} {match}"
|
||||
)
|
||||
|
||||
|
||||
def hexint(x: str) -> int:
|
||||
@ -144,6 +217,14 @@ def main(prog: Program, argv: Sequence[str]) -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="find what is referencing a filesystem object"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-L",
|
||||
"--dereference",
|
||||
action="store_true",
|
||||
help="if the given path is a symbolic link, follow it",
|
||||
)
|
||||
|
||||
object_group = parser.add_argument_group(
|
||||
title="filesystem object selection"
|
||||
).add_mutually_exclusive_group(required=True)
|
||||
@ -156,14 +237,41 @@ def main(prog: Program, argv: Sequence[str]) -> None:
|
||||
type=hexint,
|
||||
help="find references to the given struct inode pointer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-L",
|
||||
"--dereference",
|
||||
action="store_true",
|
||||
help="if the given path is a symbolic link, follow it",
|
||||
object_group.add_argument(
|
||||
"--super-block",
|
||||
metavar="PATH",
|
||||
help="find references to the filesystem (super block) containing the given path",
|
||||
)
|
||||
object_group.add_argument(
|
||||
"--super-block-pointer",
|
||||
metavar="ADDRESS",
|
||||
type=hexint,
|
||||
help="find references to the given struct super_block pointer",
|
||||
)
|
||||
|
||||
CHECKS = [
|
||||
"mounts",
|
||||
"tasks",
|
||||
]
|
||||
check_group = parser.add_argument_group(
|
||||
title="check selection"
|
||||
).add_mutually_exclusive_group()
|
||||
check_group.add_argument(
|
||||
"--check",
|
||||
choices=CHECKS,
|
||||
action="append",
|
||||
help="only check for references from the given source; may be given multiple times (default: all)",
|
||||
)
|
||||
check_group.add_argument(
|
||||
"--no-check",
|
||||
choices=CHECKS,
|
||||
action="append",
|
||||
help="don't check for references from the given source; may be given multiple times",
|
||||
)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
visitor: "Visitor"
|
||||
if args.inode is not None:
|
||||
fd = os.open(args.inode, os.O_PATH | (0 if args.dereference else os.O_NOFOLLOW))
|
||||
try:
|
||||
@ -172,10 +280,37 @@ def main(prog: Program, argv: Sequence[str]) -> None:
|
||||
os.close(fd)
|
||||
elif args.inode_pointer is not None:
|
||||
visitor = InodeVisitor(Object(prog, "struct inode *", args.inode_pointer))
|
||||
elif args.super_block is not None:
|
||||
fd = os.open(
|
||||
args.super_block, os.O_PATH | (0 if args.dereference else os.O_NOFOLLOW)
|
||||
)
|
||||
try:
|
||||
visitor = SuperBlockVisitor(
|
||||
fget(find_task(prog, os.getpid()), fd).f_inode.i_sb
|
||||
)
|
||||
finally:
|
||||
os.close(fd)
|
||||
elif args.super_block_pointer is not None:
|
||||
visitor = SuperBlockVisitor(
|
||||
Object(prog, "struct super_block *", args.super_block_pointer)
|
||||
)
|
||||
else:
|
||||
assert False
|
||||
|
||||
visit_tasks(prog, visitor)
|
||||
if args.check:
|
||||
enabled_checks = set(args.check)
|
||||
else:
|
||||
enabled_checks = set(CHECKS)
|
||||
if args.no_check:
|
||||
enabled_checks -= set(args.no_check)
|
||||
|
||||
if "mounts" in enabled_checks or "tasks" in enabled_checks:
|
||||
visit_tasks(
|
||||
prog,
|
||||
visitor,
|
||||
check_mounts="mounts" in enabled_checks,
|
||||
check_tasks="tasks" in enabled_checks,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
Loading…
Reference in New Issue
Block a user