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:
Omar Sandoval 2024-03-01 14:25:06 -08:00
parent f8851c54f0
commit bb137f1887
3 changed files with 308 additions and 72 deletions

View File

@ -348,6 +348,45 @@ def mlock(addr, len):
_check_ctypes_syscall(_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 = _c.syscall
_syscall.restype = ctypes.c_long _syscall.restype = ctypes.c_long

View File

@ -7,11 +7,20 @@ import io
import mmap import mmap
import os import os
from pathlib import Path from pathlib import Path
import re
import sys import sys
import tempfile import tempfile
from drgn.helpers.linux.fs import fget
from drgn.helpers.linux.pid import find_task 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 from tools.fsrefs import main
@ -41,7 +50,7 @@ class TestFsRefs(LinuxKernelTestCase):
fd = os.open(path, os.O_CREAT | os.O_WRONLY, 0o600) fd = os.open(path, os.O_CREAT | os.O_WRONLY, 0o600)
try: try:
self.assertRegex( self.assertRegex(
self.run_and_capture("--inode", str(path)), self.run_and_capture("--check", "tasks", "--inode", str(path)),
rf"pid {os.getpid()} \(.*\) fd {fd} ", rf"pid {os.getpid()} \(.*\) fd {fd} ",
) )
finally: finally:
@ -57,7 +66,7 @@ class TestFsRefs(LinuxKernelTestCase):
try: try:
link_fd = os.open(link, os.O_PATH | os.O_NOFOLLOW) link_fd = os.open(link, os.O_PATH | os.O_NOFOLLOW)
try: try:
output = self.run_and_capture("--inode", str(link)) output = self.run_and_capture("--check", "tasks", "--inode", str(link))
self.assertNotRegex( self.assertNotRegex(
output, output,
rf"pid {os.getpid()} \(.*\) fd {file_fd} ", rf"pid {os.getpid()} \(.*\) fd {file_fd} ",
@ -67,7 +76,9 @@ class TestFsRefs(LinuxKernelTestCase):
rf"pid {os.getpid()} \(.*\) fd {link_fd} ", 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( self.assertRegex(
output, output,
rf"pid {os.getpid()} \(.*\) fd {file_fd} ", rf"pid {os.getpid()} \(.*\) fd {file_fd} ",
@ -89,7 +100,7 @@ class TestFsRefs(LinuxKernelTestCase):
path = self._tmp / "dir" path = self._tmp / "dir"
with fork_and_sigwait(mkdir_and_chdir, path, 0o600) as pid: with fork_and_sigwait(mkdir_and_chdir, path, 0o600) as pid:
self.assertRegex( self.assertRegex(
self.run_and_capture("--inode", str(path)), self.run_and_capture("--check", "tasks", "--inode", str(path)),
rf"pid {pid} \(.*\) cwd ", rf"pid {pid} \(.*\) cwd ",
) )
@ -101,13 +112,15 @@ class TestFsRefs(LinuxKernelTestCase):
path = self._tmp / "dir" path = self._tmp / "dir"
with fork_and_sigwait(mkdir_and_chroot, path, 0o600) as pid: with fork_and_sigwait(mkdir_and_chroot, path, 0o600) as pid:
self.assertRegex( self.assertRegex(
self.run_and_capture("--inode", str(path)), self.run_and_capture("--check", "tasks", "--inode", str(path)),
rf"pid {pid} \(.*\) root ", rf"pid {pid} \(.*\) root ",
) )
def test_exe(self): def test_exe(self):
self.assertRegex( 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 ", rf"pid {os.getpid()} \(.*\) exe ",
) )
@ -121,16 +134,65 @@ class TestFsRefs(LinuxKernelTestCase):
start = ctypes.addressof(ctypes.c_char.from_buffer(map)) start = ctypes.addressof(ctypes.c_char.from_buffer(map))
end = start + mmap.PAGESIZE end = start + mmap.PAGESIZE
self.assertRegex( 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)} ", rf"pid {os.getpid()} \(.*\) vma {hex(start)}-{hex(end)} ",
) )
def test_inode_pointer(self): def test_inode_pointer(self):
self.assertRegex( self.assertRegex(
self.run_and_capture( self.run_and_capture(
"--check",
"tasks",
"--inode-pointer", "--inode-pointer",
hex(find_task(self.prog, os.getpid()).mm.exe_file.f_inode), hex(find_task(self.prog, os.getpid()).mm.exe_file.f_inode),
"--dereference", "--dereference",
), ),
rf"pid {os.getpid()} \(.*\) exe ", 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),
),
)

View File

@ -7,36 +7,17 @@ import typing
from typing import Any, Callable, Optional, Sequence, Union from typing import Any, Callable, Optional, Sequence, Union
from drgn import FaultError, Object, Program 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.mm import for_each_vma
from drgn.helpers.linux.pid import find_task, for_each_task 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: class warn_on_fault:
def __init__(self, message: Union[str, Callable[[], str]]) -> None: def __init__(self, message: Union[str, Callable[[], str]]) -> None:
@ -61,13 +42,84 @@ class warn_on_fault:
ignore_fault = warn_on_fault("") ignore_fault = warn_on_fault("")
format_args = { format_args = {
"dereference": False, "dereference": False,
"symbolize": 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"): with warn_on_fault("iterating tasks"):
for task in for_each_task(prog): for task in for_each_task(prog):
cached_task_id = None cached_task_id = None
@ -102,38 +154,59 @@ def visit_tasks(prog: Program, visitor: "Visitor") -> None:
if mm and mm == group_leader.mm: if mm and mm == group_leader.mm:
mm = None mm = None
if files: if check_mounts:
for fd, file in for_each_file(task): 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: with ignore_fault:
if visitor.visit_file(file): match = visitor.visit_path(fs.root.address_of_())
print( if match:
f"{task_id()} fd {fd} {file.format_(**format_args)}" print(f"{task_id()} root {match}")
)
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):
with ignore_fault: with ignore_fault:
file = vma.vm_file.read_() match = visitor.visit_path(fs.pwd.address_of_())
if file and visitor.visit_file(file): if match:
print( print(f"{task_id()} cwd {match}")
f"{task_id()} vma {hex(vma.vm_start)}-{hex(vma.vm_end)} {vma.format_(**format_args)}"
) 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: def hexint(x: str) -> int:
@ -144,6 +217,14 @@ def main(prog: Program, argv: Sequence[str]) -> None:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="find what is referencing a filesystem object" 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( object_group = parser.add_argument_group(
title="filesystem object selection" title="filesystem object selection"
).add_mutually_exclusive_group(required=True) ).add_mutually_exclusive_group(required=True)
@ -156,14 +237,41 @@ def main(prog: Program, argv: Sequence[str]) -> None:
type=hexint, type=hexint,
help="find references to the given struct inode pointer", help="find references to the given struct inode pointer",
) )
parser.add_argument( object_group.add_argument(
"-L", "--super-block",
"--dereference", metavar="PATH",
action="store_true", help="find references to the filesystem (super block) containing the given path",
help="if the given path is a symbolic link, follow it",
) )
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) args = parser.parse_args(argv)
visitor: "Visitor"
if args.inode is not None: if args.inode is not None:
fd = os.open(args.inode, os.O_PATH | (0 if args.dereference else os.O_NOFOLLOW)) fd = os.open(args.inode, os.O_PATH | (0 if args.dereference else os.O_NOFOLLOW))
try: try:
@ -172,10 +280,37 @@ def main(prog: Program, argv: Sequence[str]) -> None:
os.close(fd) os.close(fd)
elif args.inode_pointer is not None: elif args.inode_pointer is not None:
visitor = InodeVisitor(Object(prog, "struct inode *", args.inode_pointer)) 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: else:
assert False 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__": if __name__ == "__main__":