Skip to content

Commit

Permalink
tools/fsrefs.py: check for references from uprobes
Browse files Browse the repository at this point in the history
This one is pretty involved to implement and to test.

Signed-off-by: Omar Sandoval <[email protected]>
  • Loading branch information
osandov committed Mar 7, 2024
1 parent 3272a62 commit a5da128
Show file tree
Hide file tree
Showing 5 changed files with 416 additions and 19 deletions.
86 changes: 86 additions & 0 deletions tests/linux_kernel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,92 @@ def mkswap(path, size):
f.write(header)


class _perf_event_attr_sample_period_or_freq(ctypes.Union):
_fields_ = (
("sample_period", ctypes.c_uint64),
("sample_freq", ctypes.c_uint64),
)


class _perf_event_attr_wakeup_events_or_watermark(ctypes.Union):
_fields_ = (
("wakeup_events", ctypes.c_uint32),
("wakeup_watermark", ctypes.c_uint32),
)


class _perf_event_attr_config1(ctypes.Union):
_fields_ = (
("bp_addr", ctypes.c_uint64),
("kprobe_func", ctypes.c_uint64),
("uprobe_path", ctypes.c_uint64),
("config1", ctypes.c_uint64),
)


class _perf_event_attr_config2(ctypes.Union):
_fields_ = (
("bp_len", ctypes.c_uint64),
("kprobe_addr", ctypes.c_uint64),
("probe_offset", ctypes.c_uint64),
("config2", ctypes.c_uint64),
)


class perf_event_attr(ctypes.Structure):
_fields_ = (
("type", ctypes.c_uint32),
("size", ctypes.c_uint32),
("config", ctypes.c_uint64),
("_sample_period_or_freq", _perf_event_attr_sample_period_or_freq),
("sample_type", ctypes.c_uint64),
("read_format", ctypes.c_uint64),
("_bitfields1", ctypes.c_uint64),
("_wakeup_events_or_watermark", _perf_event_attr_wakeup_events_or_watermark),
("bp_type", ctypes.c_uint32),
("_config1", _perf_event_attr_config1),
("_config2", _perf_event_attr_config2),
("branch_sample_type", ctypes.c_uint64),
("sample_regs_user", ctypes.c_uint64),
("sample_stack_user", ctypes.c_uint32),
("clockid", ctypes.c_int32),
("sample_regs_intr", ctypes.c_uint64),
("aux_watermark", ctypes.c_uint32),
("sample_max_stack", ctypes.c_uint16),
("__reserved2", ctypes.c_uint16),
("aux_sample_size", ctypes.c_uint32),
("__reserved3", ctypes.c_uint32),
("sig_data", ctypes.c_uint64),
("config3", ctypes.c_uint64),
)
_anonymous_ = (
"_sample_period_or_freq",
"_wakeup_events_or_watermark",
"_config1",
"_config2",
)


PERF_FLAG_FD_NO_GROUP = 1 << 0
PERF_FLAG_FD_OUTPUT = 1 << 1
PERF_FLAG_PID_CGROUP = 1 << 2
PERF_FLAG_FD_CLOEXEC = 1 << 3


def perf_event_open(attr, pid, cpu, group_fd=-1, flags=PERF_FLAG_FD_CLOEXEC):
attr.size = ctypes.sizeof(perf_event_attr)
return _check_ctypes_syscall(
_syscall(
SYS["perf_event_open"],
ctypes.byref(attr),
ctypes.c_int(pid),
ctypes.c_int(cpu),
ctypes.c_int(group_fd),
ctypes.c_ulong(flags),
)
)


_syscall = _c.syscall
_syscall.restype = ctypes.c_long

Expand Down
111 changes: 109 additions & 2 deletions tests/linux_kernel/tools/test_fsrefs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# SPDX-License-Identifier: LGPL-2.1-or-later

import array
import contextlib
import ctypes
import errno
Expand All @@ -9,9 +10,11 @@
import os
from pathlib import Path
import re
import socket
import subprocess
import sys
import tempfile
import unittest

from drgn.helpers.linux.fs import fget
from drgn.helpers.linux.pid import find_task
Expand All @@ -28,6 +31,8 @@
losetup,
mkswap,
mount,
perf_event_attr,
perf_event_open,
skip_unless_have_test_disk,
swapoff,
swapon,
Expand All @@ -36,6 +41,8 @@
)
from tools.fsrefs import main

UPROBE_TYPE_PATH = Path("/sys/bus/event_source/devices/uprobe/type")


class TestFsRefs(LinuxKernelTestCase):
def setUp(self):
Expand Down Expand Up @@ -248,7 +255,7 @@ def setup_binfmt_misc_in_userns(path):
unshare(CLONE_NEWUSER | CLONE_NEWNS)
except OSError as e:
if e.errno == errno.EINVAL:
return "kernel does not support user namespaces"
return "kernel does not support user namespaces (CONFIG_USER_NS)"
else:
raise
Path("/proc/self/uid_map").write_text("0 0 1")
Expand All @@ -266,7 +273,7 @@ def setup_binfmt_misc_in_userns(path):
)
except OSError as e:
if e.errno == errno.ENODEV:
return "kernel does not support binfmt_misc"
return "kernel does not support binfmt_misc (CONFIG_BINFMT_MISC)"
elif e.errno == errno.EPERM:
return "kernel does not support sandboxed binfmt_misc mounts"
else:
Expand Down Expand Up @@ -317,3 +324,103 @@ def test_swap_file(self):
"swap file (struct swap_info_struct *)",
self.run_and_capture("--check", "swap", "--inode", str(path)),
)

def test_uprobe_event(self):
for mnt in iter_mounts():
if mnt.fstype == "tracefs":
break
else:
self.skipTest("tracefs not mounted")
uprobe_events = mnt.mount_point / "uprobe_events"
if not uprobe_events.exists():
self.skipTest(
"kernel does not support uprobe events (CONFIG_UPROBE_EVENTS)"
)

def uprobe_events_append(s):
# open(..., "a") tries lseek(..., SEEK_END), which fails with
# EINVAL.
with open(os.open(uprobe_events, os.O_WRONLY | os.O_APPEND), "w") as f:
f.write(s)

path = self._tmp / "file"
path.touch()

probe_name = f"drgntest_{os.urandom(20).hex()}"
retprobe_name = f"drgntest_{os.urandom(20).hex()}"
with contextlib.ExitStack() as exit_stack:
uprobe_events_append(f"p:{probe_name} {path}:0\n")
exit_stack.callback(uprobe_events_append, f"-:{probe_name}\n")
uprobe_events_append(f"r:{retprobe_name} {path}:0\n")
exit_stack.callback(uprobe_events_append, f"-:{retprobe_name}\n")

instance = Path(tempfile.mkdtemp(dir=mnt.mount_point / "instances"))
exit_stack.callback(instance.rmdir)

(instance / "events/uprobes" / probe_name / "enable").write_text("1")
(instance / "events/uprobes" / retprobe_name / "enable").write_text("1")

output = self.run_and_capture("--check", "uprobes", "--inode", str(path))
self.assertIn(f"uprobe event p:uprobes/{probe_name} ", output)
self.assertIn(f"uprobe event r:uprobes/{retprobe_name} ", output)

@unittest.skipUnless(
UPROBE_TYPE_PATH.exists(), "kernel does not support perf_uprobe"
)
def test_perf_uprobe(self):
path = self._tmp / "file"
path.touch()

attr = perf_event_attr()
attr.type = int(UPROBE_TYPE_PATH.read_text())
ctypes_path = ctypes.c_char_p(os.fsencode(path))
attr.uprobe_path = ctypes.cast(ctypes_path, ctypes.c_void_p).value
fd = perf_event_open(attr, -1, min(os.sched_getaffinity(0)))
try:
self.assertIn(
f"perf uprobe (owned by pid {os.getpid()}",
self.run_and_capture("--check", "uprobes", "--inode", str(path)),
)
finally:
os.close(fd)

@unittest.skipUnless(
UPROBE_TYPE_PATH.exists(), "kernel does not support perf_uprobe"
)
def test_perf_uprobe_no_owner(self):
path = self._tmp / "file"
path.touch()

sock1, sock2 = socket.socketpair()
try:
# Create a perf event in a process, send it over a Unix socket to
# keep it alive, then die.
pid = os.fork()
if pid == 0:
try:
attr = perf_event_attr()
attr.type = int(UPROBE_TYPE_PATH.read_text())
ctypes_path = ctypes.c_char_p(os.fsencode(path))
attr.uprobe_path = ctypes.cast(ctypes_path, ctypes.c_void_p).value
fd = perf_event_open(attr, -1, min(os.sched_getaffinity(0)))
sock2.sendmsg(
[b"\0"],
[
(
socket.SOL_SOCKET,
socket.SCM_RIGHTS,
array.array("i", [fd]),
)
],
)
finally:
os._exit(0)

os.waitpid(pid, 0)
self.assertIn(
"perf uprobe (no owner)",
self.run_and_capture("--check", "uprobes", "--inode", str(path)),
)
finally:
sock1.close()
sock2.close()
Loading

0 comments on commit a5da128

Please sign in to comment.