From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8DF5734F48D for ; Thu, 16 Apr 2026 12:00:34 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=170.10.133.124 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776340838; cv=none; b=iT9Q+LqyA84TdXhO5B3o76H84/eICZNo7Z2EyTcUesE4cGjzgoz5yks1itwLnQL9ImAh4NLUwZ2hDZ9Y5bolkkOHz0QU7FCVqfXn2TuflOC1e+DKCEgHIEkrsGZQPeP7ldl2v5LGmtC7MRGe4NzBOI6ivhoFHGr8VNIhoh7bE2s= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776340838; c=relaxed/simple; bh=aIb4wJlunavfVcF91OkvxU4b6KAR+1ZzBx7WbWnSh4I=; h=Message-ID:Subject:From:To:Cc:Date:In-Reply-To:References: Content-Type:MIME-Version; b=GDGM4bbSlBdKQK0rGVdeG3x0sd3X804ePjiqFX98tsFpkpLlEdxClYfxKoQ4Yeg+hHOKuP2d4gA/PjGzykPB9iQHNCItO2KLXiXH89Tf5UoyvVYQOglNMrP2v6sm6DYPSJ3VjwRbnFPsk2Ua4Jv0QfODwWnvKS/OtKI5/ubJTb4= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com; spf=pass smtp.mailfrom=redhat.com; dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com header.b=WrzV+JEC; dkim=pass (2048-bit key) header.d=redhat.com header.i=@redhat.com header.b=G2zd2DWK; arc=none smtp.client-ip=170.10.133.124 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=redhat.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com header.b="WrzV+JEC"; dkim=pass (2048-bit key) header.d=redhat.com header.i=@redhat.com header.b="G2zd2DWK" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1776340833; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:autocrypt:autocrypt; bh=+iddxLY9XhnCcM5CUn7Ti8EdGeGCr2iy+lA8qquuunA=; b=WrzV+JECSIR5CwyZsYVag0k2UK76GqLyBIxYIq0hLT5VtvMpUx6D9I3lqJlbgZ+bAU0gg1 Op0k3azv/Zi/vjSEPAYAMVkQQ3hR7wEUs+Ohy1flPnKzg46op8HG5gaBlz9DA1X0apUNsk evHQwTYiiMmVAld0eU/QfRnfpY91/rU= Received: from mail-wr1-f71.google.com (mail-wr1-f71.google.com [209.85.221.71]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-314-jBxuVx6lOFieKC0J2P2BiQ-1; Thu, 16 Apr 2026 08:00:31 -0400 X-MC-Unique: jBxuVx6lOFieKC0J2P2BiQ-1 X-Mimecast-MFC-AGG-ID: jBxuVx6lOFieKC0J2P2BiQ_1776340830 Received: by mail-wr1-f71.google.com with SMTP id ffacd0b85a97d-43d03db814eso8373526f8f.2 for ; Thu, 16 Apr 2026 05:00:30 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=google; t=1776340830; x=1776945630; darn=vger.kernel.org; h=mime-version:user-agent:content-transfer-encoding:autocrypt :references:in-reply-to:date:cc:to:from:subject:message-id:from:to :cc:subject:date:message-id:reply-to; bh=+iddxLY9XhnCcM5CUn7Ti8EdGeGCr2iy+lA8qquuunA=; b=G2zd2DWK8P0X2PxC1WYOiuG1qPAFCB+vk4OJ3tdiNgefi572FBcEPMbGNuBD8bAyVC DUgA8rB+KbzgqmfPooga50n5YH1154a3XuWEjszPjMRoJwqlwSjnmRlDZLsZ6r/iUMAt 5V2HBx1aRs9aDKVPXEWWVwfB7tbv2fidDgl5Zhi/M+U68C63h3oG0xEFcqxn5QShhgja K4JQ8ruk3iPonhD+gUGcXpyJxo5pwxGQJ5RtHazXZKRhu8xVNKeW2KEGdrgzvCWCqrF8 74qHhhI4kLUy29yTHGbjOfZHn/c+IC0zcreVOb8Ep496sGd3LbZB1CeZRxpvrpy9EKCC IrwQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1776340830; x=1776945630; h=mime-version:user-agent:content-transfer-encoding:autocrypt :references:in-reply-to:date:cc:to:from:subject:message-id:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=+iddxLY9XhnCcM5CUn7Ti8EdGeGCr2iy+lA8qquuunA=; b=IEkzoiEZpij6EpNpGsSaI/NyBn3Sw30YSuq2xNuq2mtQ4+07wSHwFvKnIHFKBhvUij 6+XKVihQlKimNt0Hde8lqp+H6mdslgQp+dDWeq1+uHikE4m8hau9t6b17JhXQjZeWTkQ p4kLoI/RbRqTmkeLsKi5+f32gjcZA+DqhRw6D8qmXBVRU4jQHiStTK4mmbrxXOK0VcvI l2uKrFwaCWaeDqgrEh7ECjGbnH7vOC+xcjtBwTVeu9vC5Z6XuGS9lnnvA+nXpF3+Kpwa IFL+m7Col8YVEi47WPhyaD87btdgaY9JCRjoeF0ePPOJ19RMdWcxqGOWAUu0WTYkrAMs 9gsw== X-Forwarded-Encrypted: i=1; AFNElJ8u8MfHk8ILa9XzelFRQeQ5+6og9YVd3XnlmUrsNKrelFy2f1Rsd1p7LO1yxVQW7SAow0zg1fLyBS065cI=@vger.kernel.org X-Gm-Message-State: AOJu0YybldwwN8lC+a+0Wm5fxvNEKvOD3Gv2Y2ODHfsXpgztfz8xRvMk 7JH3KgeITmfXlSxfamwVPqUniZNMEVP6DF0JXw37yliMS00HzNNVzaiA25eraZ8JiXEu10hbgJu bmD6qc+ynQ/e8w2o8wIanK6GWwQxJW+8kpLaV5djPbaltP+oBeMJZJroXGG53OfkS/g== X-Gm-Gg: AeBDietvJ95zWXOFXaJvDZrE0TjeiXWW3jRh9euKCjTV4gqpiLNFPCIV2829EWHtkIx D1F7mXFQP/7FbVNaYdFIu4E6yK2IYzZCQoGxhFqGVgrAoqltZtV3HLWWtkb9tZZgYF2yAoaGLJv 0Cmd7PVCe9XsIydo/YdPlbK7nZUVY1SKY83Dde9ZwK7gALlhPOr3y9KonqoRPiCFfYmK92xhRgK D41ewDT043MbHTfSd43oUruZxeG3miDZE53dCdneucLu6ZBLpWIs1JH0UVPi4SGdN26zJ3p/1M5 2G3CMlh0SK1GSl6xAQC83LQO0LknFMhVfE5CeYNU2w6Am8l2Chg/l0Qaf8ThXp40wo1+0QQPwV9 Jrd+1woigD+BsUhyD40hMwESTjZYAgM21NzHRT6x0kjhS5IquTamFMh6Mx0Cwu1luykbiwMMbiP wQ+Wp7/a4KU3SJQrgLz8rlyvyzNg== X-Received: by 2002:a05:600c:5308:b0:488:8d44:bf98 with SMTP id 5b1f17b1804b1-488d67f088amr364156315e9.7.1776340828890; Thu, 16 Apr 2026 05:00:28 -0700 (PDT) X-Received: by 2002:a05:600c:5308:b0:488:8d44:bf98 with SMTP id 5b1f17b1804b1-488d67f088amr364152365e9.7.1776340826003; Thu, 16 Apr 2026 05:00:26 -0700 (PDT) Received: from gmonaco-thinkpadt14gen3.rmtit.csb (212-8-243-115.hosted-by-worldstream.net. [212.8.243.115]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-488f584e306sm57002935e9.11.2026.04.16.05.00.24 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 16 Apr 2026 05:00:25 -0700 (PDT) Message-ID: <3606de6182988ef9ebb9bdd3c8bbfa04c7f59ebb.camel@redhat.com> Subject: Re: [RFC PATCH 4/4] selftests/rv: Add selftest for the tlob monitor From: Gabriele Monaco To: wen.yang@linux.dev Cc: linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org, Steven Rostedt , Masami Hiramatsu , Mathieu Desnoyers Date: Thu, 16 Apr 2026 14:00:22 +0200 In-Reply-To: <5bdd82dd8aeb1d3f955b727ae1fce9819b35c170.1776020428.git.wen.yang@linux.dev> References: <5bdd82dd8aeb1d3f955b727ae1fce9819b35c170.1776020428.git.wen.yang@linux.dev> Autocrypt: addr=gmonaco@redhat.com; prefer-encrypt=mutual; keydata=mDMEZuK5YxYJKwYBBAHaRw8BAQdAmJ3dM9Sz6/Hodu33Qrf8QH2bNeNbOikqYtxWFLVm0 1a0JEdhYnJpZWxlIE1vbmFjbyA8Z21vbmFjb0BrZXJuZWwub3JnPoiZBBMWCgBBFiEEysoR+AuB3R Zwp6j270psSVh4TfIFAmjKX2MCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgk Q70psSVh4TfIQuAD+JulczTN6l7oJjyroySU55Fbjdvo52xiYYlMjPG7dCTsBAMFI7dSL5zg98I+8 cXY1J7kyNsY6/dcipqBM4RMaxXsOtCRHYWJyaWVsZSBNb25hY28gPGdtb25hY29AcmVkaGF0LmNvb T6InAQTFgoARAIbAwUJBaOagAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgBYhBMrKEfgLgd0WcK eo9u9KbElYeE3yBQJoymCyAhkBAAoJEO9KbElYeE3yjX4BAJ/ETNnlHn8OjZPT77xGmal9kbT1bC1 7DfrYVISWV2Y1AP9HdAMhWNAvtCtN2S1beYjNybuK6IzWYcFfeOV+OBWRDQ== Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable User-Agent: Evolution 3.58.3 (3.58.3-1.fc43) Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 On Mon, 2026-04-13 at 03:27 +0800, wen.yang@linux.dev wrote: > From: Wen Yang >=20 > Add a kselftest suite (TAP output, 19 test points) for the tlob RV > monitor under tools/testing/selftests/rv/. >=20 > test_tlob.sh drives a compiled C helper (tlob_helper) and, for uprobe > tests, a target binary (tlob_uprobe_target). Coverage spans the > tracefs enable/disable path, uprobe-triggered violations, and the > ioctl interface (within-budget stop, CPU-bound and sleep violations, > duplicate start, ring buffer mmap and consumption). >=20 > Requires CONFIG_RV_MON_TLOB=3Dy and CONFIG_RV_CHARDEV=3Dy; must be run > as root. >=20 > Signed-off-by: Wen Yang Those are some extensive selftests! Could you integrate them with the existing test suite under tools/testing/selftests/verification ? You would probably just get your tlob_helper built and call it from some sh= ell script under test.d, the harness should work without the need for extra hel= pers. Thanks, Gabriele > --- > =C2=A0tools/include/uapi/linux/rv.h=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 |=C2=A0 54 + > =C2=A0tools/testing/selftests/rv/Makefile=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 |=C2=A0 18 + > =C2=A0tools/testing/selftests/rv/test_tlob.sh=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0 | 563 ++++++++++ > =C2=A0tools/testing/selftests/rv/tlob_helper.c=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 | 994 ++++++++++++++++++ > =C2=A0.../testing/selftests/rv/tlob_uprobe_target.c | 108 ++ > =C2=A05 files changed, 1737 insertions(+) > =C2=A0create mode 100644 tools/include/uapi/linux/rv.h > =C2=A0create mode 100644 tools/testing/selftests/rv/Makefile > =C2=A0create mode 100755 tools/testing/selftests/rv/test_tlob.sh > =C2=A0create mode 100644 tools/testing/selftests/rv/tlob_helper.c > =C2=A0create mode 100644 tools/testing/selftests/rv/tlob_uprobe_target.c >=20 > diff --git a/tools/include/uapi/linux/rv.h b/tools/include/uapi/linux/rv.= h > new file mode 100644 > index 000000000..bef07aded > --- /dev/null > +++ b/tools/include/uapi/linux/rv.h > @@ -0,0 +1,54 @@ > +/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ > +/* > + * UAPI definitions for Runtime Verification (RV) monitors. > + * > + * This is a tools-friendly copy of include/uapi/linux/rv.h. > + * Keep in sync with the kernel header. > + */ > + > +#ifndef _UAPI_LINUX_RV_H > +#define _UAPI_LINUX_RV_H > + > +#include > +#include > + > +/* Magic byte shared by all RV monitor ioctls. */ > +#define RV_IOC_MAGIC 0xB9 > + > +/* ---------------------------------------------------------------------= -- > + * tlob: task latency over budget monitor=C2=A0 (nr 0x01 - 0x1F) > + * ---------------------------------------------------------------------= -- > + */ > + > +struct tlob_start_args { > + __u64 threshold_us; > + __u64 tag; > + __s32 notify_fd; > + __u32 flags; > +}; > + > +struct tlob_event { > + __u32 tid; > + __u32 pad; > + __u64 threshold_us; > + __u64 on_cpu_us; > + __u64 off_cpu_us; > + __u32 switches; > + __u32 state;=C2=A0=C2=A0 /* 1 =3D on_cpu, 0 =3D off_cpu */ > + __u64 tag; > +}; > + > +struct tlob_mmap_page { > + __u32=C2=A0 data_head; > + __u32=C2=A0 data_tail; > + __u32=C2=A0 capacity; > + __u32=C2=A0 version; > + __u32=C2=A0 data_offset; > + __u32=C2=A0 record_size; > + __u64=C2=A0 dropped; > +}; > + > +#define TLOB_IOCTL_TRACE_START _IOW(RV_IOC_MAGIC, 0x01, struct > tlob_start_args) > +#define TLOB_IOCTL_TRACE_STOP _IO(RV_IOC_MAGIC,=C2=A0 0x02) > + > +#endif /* _UAPI_LINUX_RV_H */ > diff --git a/tools/testing/selftests/rv/Makefile > b/tools/testing/selftests/rv/Makefile > new file mode 100644 > index 000000000..14e94a1ab > --- /dev/null > +++ b/tools/testing/selftests/rv/Makefile > @@ -0,0 +1,18 @@ > +# SPDX-License-Identifier: GPL-2.0 > +# Makefile for rv selftests > + > +TEST_GEN_PROGS :=3D tlob_helper tlob_uprobe_target > + > +TEST_PROGS :=3D \ > + test_tlob.sh \ > + > +# TOOLS_INCLUDES is defined by ../lib.mk; provides -isystem to > +# tools/include/uapi so that #include resolves to the > +# in-tree UAPI header without requiring make headers_install. > +# Note: both must be added to the global variables, not as target-specif= ic > +# overrides, because lib.mk rewrites TEST_GEN_PROGS to $(OUTPUT)/name > +# before per-target rules would be evaluated. > +CFLAGS +=3D $(TOOLS_INCLUDES) > +LDLIBS +=3D -lpthread > + > +include ../lib.mk > diff --git a/tools/testing/selftests/rv/test_tlob.sh > b/tools/testing/selftests/rv/test_tlob.sh > new file mode 100755 > index 000000000..3ba2125eb > --- /dev/null > +++ b/tools/testing/selftests/rv/test_tlob.sh > @@ -0,0 +1,563 @@ > +#!/bin/sh > +# SPDX-License-Identifier: GPL-2.0 > +# > +# Selftest for the tlob (task latency over budget) RV monitor. > +# > +# Two interfaces are tested: > +# > +#=C2=A0=C2=A0 1. tracefs interface: > +#=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 enable/disable, presence of = tracefs files, > +#=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 uprobe binding (threshold_us= :offset_start:offset_stop:binary_path) > and > +#=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 violation detection via the = ftrace ring buffer. > +# > +#=C2=A0=C2=A0 2. /dev/rv ioctl self-instrumentation (via tlob_helper): > +#=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 within-budget, over-budget o= n-CPU, over-budget off-CPU (sleep), > +#=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 double-start, stop-without-s= tart. > +# > +# Written to be POSIX sh compatible (no bash-specific extensions). > + > +ksft_skip=3D4 > +t_pass=3D0; t_fail=3D0; t_skip=3D0; t_total=3D0 > + > +tap_header() { echo "TAP version 13"; } > +tap_plan()=C2=A0=C2=A0 { echo "1..$1"; } > +tap_pass()=C2=A0=C2=A0 { t_pass=3D$((t_pass+1)); echo "ok $t_total - $1"= ; } > +tap_fail()=C2=A0=C2=A0 { t_fail=3D$((t_fail+1)); echo "not ok $t_total -= $1" > +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0 [ -n "$2" ] && echo "=C2=A0 # $2"; } > +tap_skip()=C2=A0=C2=A0 { t_skip=3D$((t_skip+1)); echo "ok $t_total - $1 = # SKIP $2"; } > +next_test()=C2=A0 { t_total=3D$((t_total+1)); } > + > +TRACEFS=3D$(grep -m1 tracefs /proc/mounts 2>/dev/null | awk '{print $2}'= ) > +[ -z "$TRACEFS" ] && TRACEFS=3D/sys/kernel/tracing > + > +RV_DIR=3D"${TRACEFS}/rv" > +TLOB_DIR=3D"${RV_DIR}/monitors/tlob" > +TRACE_FILE=3D"${TRACEFS}/trace" > +TRACING_ON=3D"${TRACEFS}/tracing_on" > +TLOB_MONITOR=3D"${TLOB_DIR}/monitor" > +BUDGET_EXCEEDED_ENABLE=3D"${TRACEFS}/events/rv/tlob_budget_exceeded/enab= le" > +RV_DEV=3D"/dev/rv" > + > +# tlob_helper and tlob_uprobe_target must be in the same directory as > +# this script or on PATH. > +SCRIPT_DIR=3D$(dirname "$0") > +IOCTL_HELPER=3D"${SCRIPT_DIR}/tlob_helper" > +UPROBE_TARGET=3D"${SCRIPT_DIR}/tlob_uprobe_target" > + > +check_root()=C2=A0=C2=A0=C2=A0=C2=A0 { [ "$(id -u)" =3D "0" ] || { echo = "# Need root" >&2; exit > $ksft_skip; }; } > +check_tracefs()=C2=A0 { [ -d "${TRACEFS}" ]=C2=A0=C2=A0 || { echo "# No = tracefs" >&2; exit > $ksft_skip; }; } > +check_rv_dir()=C2=A0=C2=A0 { [ -d "${RV_DIR}" ]=C2=A0=C2=A0=C2=A0 || { e= cho "# No RV infra" >&2; exit > $ksft_skip; }; } > +check_tlob()=C2=A0=C2=A0=C2=A0=C2=A0 { [ -d "${TLOB_DIR}" ]=C2=A0 || { e= cho "# No tlob monitor" >&2; > exit $ksft_skip; }; } > + > +tlob_enable()=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 { echo 1 >= "${TLOB_DIR}/enable"; } > +tlob_disable()=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 { echo 0 > "${T= LOB_DIR}/enable" 2>/dev/null; } > +tlob_is_enabled()=C2=A0=C2=A0=C2=A0=C2=A0 { [ "$(cat "${TLOB_DIR}/enable= " 2>/dev/null)" =3D "1" ]; > } > +trace_event_enable()=C2=A0 { echo 1 > "${BUDGET_EXCEEDED_ENABLE}" 2>/dev= /null; } > +trace_event_disable() { echo 0 > "${BUDGET_EXCEEDED_ENABLE}" 2>/dev/null= ; } > +trace_on()=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0 { echo 1 > "${TRACING_ON}" 2>/dev/null; } > +trace_clear()=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 { echo > "= ${TRACE_FILE}"; } > +trace_grep()=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 { gre= p -q "$1" "${TRACE_FILE}" 2>/dev/null; } > + > +cleanup() { > + tlob_disable > + trace_event_disable > + trace_clear > +} > + > +# ----------------------------------------------------------------------= ----- > +# Test 1: enable / disable > +# ----------------------------------------------------------------------= ----- > +run_test_enable_disable() { > + next_test; cleanup > + tlob_enable > + if ! tlob_is_enabled; then > + tap_fail "enable_disable" "not enabled after echo 1"; > cleanup; return > + fi > + tlob_disable > + if tlob_is_enabled; then > + tap_fail "enable_disable" "still enabled after echo 0"; > cleanup; return > + fi > + tap_pass "enable_disable"; cleanup > +} > + > +# ----------------------------------------------------------------------= ----- > +# Test 2: tracefs files present > +# ----------------------------------------------------------------------= ----- > +run_test_tracefs_files() { > + next_test; cleanup > + missing=3D"" > + for f in enable desc monitor; do > + [ ! -e "${TLOB_DIR}/${f}" ] && missing=3D"${missing} ${f}" > + done > + [ -n "${missing}" ] \ > + && tap_fail "tracefs_files" "missing:${missing}" \ > + || tap_pass "tracefs_files" > + cleanup > +} > + > +# ----------------------------------------------------------------------= ----- > +# Helper: resolve file offset of a function inside a binary. > +# > +# Usage: resolve_offset > +# Prints the hex file offset, or empty string on failure. > +# ----------------------------------------------------------------------= ----- > +resolve_offset() { > + bin=3D$1; vaddr=3D$2 > + # Parse /proc/self/maps to find the mapping that contains vaddr. > + # Each line: start-end perms offset dev inode [path] > + while IFS=3D read -r line; do > + set -- $line > + range=3D$1; off=3D$4; path=3D$7 > + [ -z "$path" ] && continue > + # Only consider the mapping for our binary > + [ "$path" !=3D "$bin" ] && continue > + # Split range into start and end > + start=3D$(echo "$range" | cut -d- -f1) > + end=3D$(echo "$range" | cut -d- -f2) > + # Convert hex to decimal for comparison (use printf) > + s=3D$(printf "%d" "0x${start}" 2>/dev/null) || continue > + e=3D$(printf "%d" "0x${end}"=C2=A0=C2=A0 2>/dev/null) || continue > + v=3D$(printf "%d" "${vaddr}"=C2=A0=C2=A0 2>/dev/null) || continue > + o=3D$(printf "%d" "0x${off}"=C2=A0=C2=A0 2>/dev/null) || continue > + if [ "$v" -ge "$s" ] && [ "$v" -lt "$e" ]; then > + file_off=3D$(printf "0x%x" $(( (v - s) + o ))) > + echo "$file_off" > + return > + fi > + done < /proc/self/maps > +} > + > +# ----------------------------------------------------------------------= ----- > +# Test 3: uprobe binding - no false positive > +# > +# Bind this process with a 10 s budget.=C2=A0 Do nothing for 0.5 s. > +# No budget_exceeded event should appear in the trace. > +# ----------------------------------------------------------------------= ----- > +run_test_uprobe_no_false_positive() { > + next_test; cleanup > + if [ ! -e "${TLOB_MONITOR}" ]; then > + tap_skip "uprobe_no_false_positive" "monitor file not > available" > + cleanup; return > + fi > + # We probe the "sleep" command that we will run as a subprocess. > + # Use /bin/sleep as the binary; find a valid function offset (0x0 > + # resolves to the ELF entry point, which is sufficient for a > + # no-false-positive test since we just need the binding to exist). > + sleep_bin=3D$(command -v sleep 2>/dev/null) > + if [ -z "$sleep_bin" ]; then > + tap_skip "uprobe_no_false_positive" "sleep not found"; > cleanup; return > + fi > + pid=3D$$ > + # offset 0x0 probes the entry point of /bin/sleep - this is a > + # deliberate probe that will not fire during a simple 'sleep 10' > + # invoked in a subshell, but registers the pid in tlob. > + # > + # Instead, bind our own pid with a generous 10 s threshold and > + # verify that 0.5 s of idle time does NOT fire the timer. > + # > + # Since we cannot easily get a valid uprobe offset in pure shell, > + # we skip this sub-test if we cannot form a valid binding. > + exe=3D$(readlink /proc/self/exe 2>/dev/null) > + if [ -z "$exe" ]; then > + tap_skip "uprobe_no_false_positive" "cannot read > /proc/self/exe" > + cleanup; return > + fi > + trace_event_enable > + trace_on > + tlob_enable > + trace_clear > + # Sleep without any binding - just verify no spurious events > + sleep 0.5 > + trace_grep "budget_exceeded" \ > + && tap_fail "uprobe_no_false_positive" \ > + "spurious budget_exceeded without any binding" \ > + || tap_pass "uprobe_no_false_positive" > + cleanup > +} > + > +# ----------------------------------------------------------------------= ----- > +# Helper: get_uprobe_offset > +# > +# Use tlob_helper sym_offset to get the ELF file offset of > +# in .=C2=A0 Prints the hex offset (e.g. "0x11d0") or empty stri= ng on > +# failure. > +# ----------------------------------------------------------------------= ----- > +get_uprobe_offset() { > + bin=3D$1; sym=3D$2 > + if [ ! -x "${IOCTL_HELPER}" ]; then > + return > + fi > + "${IOCTL_HELPER}" sym_offset "${bin}" "${sym}" 2>/dev/null > +} > + > +# ----------------------------------------------------------------------= ----- > +# Test 4: uprobe binding - violation detected > +# > +# Start tlob_uprobe_target (a busy-spin binary with a well-known symbol)= , > +# attach a uprobe on tlob_busy_work with a 10 ms threshold, and verify > +# that a budget_expired event appears. > +# ----------------------------------------------------------------------= ----- > +run_test_uprobe_violation() { > + next_test; cleanup > + if [ ! -e "${TLOB_MONITOR}" ]; then > + tap_skip "uprobe_violation" "monitor file not available" > + cleanup; return > + fi > + if [ ! -x "${UPROBE_TARGET}" ]; then > + tap_skip "uprobe_violation" \ > + "tlob_uprobe_target not found or not executable" > + cleanup; return > + fi > + > + # Get the file offsets of the start and stop probe symbols > + busy_offset=3D$(get_uprobe_offset "${UPROBE_TARGET}" "tlob_busy_work") > + if [ -z "${busy_offset}" ]; then > + tap_skip "uprobe_violation" \ > + "cannot resolve tlob_busy_work offset in > ${UPROBE_TARGET}" > + cleanup; return > + fi > + stop_offset=3D$(get_uprobe_offset "${UPROBE_TARGET}" > "tlob_busy_work_done") > + if [ -z "${stop_offset}" ]; then > + tap_skip "uprobe_violation" \ > + "cannot resolve tlob_busy_work_done offset in > ${UPROBE_TARGET}" > + cleanup; return > + fi > + > + # Start the busy-spin target (run for 30 s so the test can observe > it) > + "${UPROBE_TARGET}" 30000 & > + busy_pid=3D$! > + sleep 0.05 > + > + trace_event_enable > + trace_on > + tlob_enable > + trace_clear > + > + # Bind the target: 10 us budget; start=3Dtlob_busy_work, > stop=3Dtlob_busy_work_done > + binding=3D"10:${busy_offset}:${stop_offset}:${UPROBE_TARGET}" > + if ! echo "${binding}" > "${TLOB_MONITOR}" 2>/dev/null; then > + kill "${busy_pid}" 2>/dev/null; wait "${busy_pid}" > 2>/dev/null > + tap_skip "uprobe_violation" \ > + "uprobe binding rejected (CONFIG_UPROBES=3Dy needed)" > + cleanup; return > + fi > + > + # Wait up to 2 s for a budget_exceeded event > + found=3D0; i=3D0 > + while [ "$i" -lt 20 ]; do > + sleep 0.1 > + trace_grep "budget_exceeded" && { found=3D1; break; } > + i=3D$((i+1)) > + done > + > + echo "-${busy_offset}:${UPROBE_TARGET}" > "${TLOB_MONITOR}" > 2>/dev/null > + kill "${busy_pid}" 2>/dev/null; wait "${busy_pid}" 2>/dev/null > + > + if [ "${found}" !=3D "1" ]; then > + tap_fail "uprobe_violation" "no budget_exceeded within 2 s" > + cleanup; return > + fi > + > + # Validate the event fields: threshold must match, on_cpu must be > non-zero > + # (CPU-bound violation), and state must be on_cpu. > + ev=3D$(grep "budget_exceeded" "${TRACE_FILE}" | head -n 1) > + if ! echo "${ev}" | grep -q "threshold=3D10 "; then > + tap_fail "uprobe_violation" "threshold field mismatch: ${ev}" > + cleanup; return > + fi > + on_cpu=3D$(echo "${ev}" | grep -o "on_cpu=3D[0-9]*" | cut -d=3D -f2) > + if [ "${on_cpu:-0}" -eq 0 ]; then > + tap_fail "uprobe_violation" "on_cpu=3D0 for a CPU-bound spin: > ${ev}" > + cleanup; return > + fi > + if ! echo "${ev}" | grep -q "state=3Don_cpu"; then > + tap_fail "uprobe_violation" "state is not on_cpu: ${ev}" > + cleanup; return > + fi > + tap_pass "uprobe_violation" > + cleanup > +} > + > +# ----------------------------------------------------------------------= ----- > +# Test 5: uprobe binding - remove binding stops monitoring > +# > +# Bind a pid via tlob_uprobe_target, then immediately remove it. > +# Verify that after removal the monitor file no longer lists the pid. > +# ----------------------------------------------------------------------= ----- > +run_test_uprobe_unbind() { > + next_test; cleanup > + if [ ! -e "${TLOB_MONITOR}" ]; then > + tap_skip "uprobe_unbind" "monitor file not available" > + cleanup; return > + fi > + if [ ! -x "${UPROBE_TARGET}" ]; then > + tap_skip "uprobe_unbind" \ > + "tlob_uprobe_target not found or not executable" > + cleanup; return > + fi > + > + busy_offset=3D$(get_uprobe_offset "${UPROBE_TARGET}" "tlob_busy_work") > + stop_offset=3D$(get_uprobe_offset "${UPROBE_TARGET}" > "tlob_busy_work_done") > + if [ -z "${busy_offset}" ] || [ -z "${stop_offset}" ]; then > + tap_skip "uprobe_unbind" \ > + "cannot resolve tlob_busy_work/tlob_busy_work_done > offset" > + cleanup; return > + fi > + > + "${UPROBE_TARGET}" 30000 & > + busy_pid=3D$! > + sleep 0.05 > + > + tlob_enable > + # 5 s budget - should not fire during this quick test > + binding=3D"5000000:${busy_offset}:${stop_offset}:${UPROBE_TARGET}" > + if ! echo "${binding}" > "${TLOB_MONITOR}" 2>/dev/null; then > + kill "${busy_pid}" 2>/dev/null; wait "${busy_pid}" > 2>/dev/null > + tap_skip "uprobe_unbind" \ > + "uprobe binding rejected (CONFIG_UPROBES=3Dy needed)" > + cleanup; return > + fi > + > + # Remove the binding > + echo "-${busy_offset}:${UPROBE_TARGET}" > "${TLOB_MONITOR}" > 2>/dev/null > + > + # The monitor file should no longer list the binding for this offset > + if grep -q "^[0-9]*:0x${busy_offset#0x}:" "${TLOB_MONITOR}" > 2>/dev/null; then > + kill "${busy_pid}" 2>/dev/null; wait "${busy_pid}" > 2>/dev/null > + tap_fail "uprobe_unbind" "pid still listed after removal" > + cleanup; return > + fi > + > + kill "${busy_pid}" 2>/dev/null; wait "${busy_pid}" 2>/dev/null > + tap_pass "uprobe_unbind" > + cleanup > +} > + > +# ----------------------------------------------------------------------= ----- > +# Test 6: uprobe - duplicate offset_start rejected > +# > +# Registering a second binding with the same offset_start in the same bi= nary > +# must be rejected with an error, since two entry uprobes at the same ad= dress > +# would cause double tlob_start_task() calls and undefined behaviour. > +# ----------------------------------------------------------------------= ----- > +run_test_uprobe_duplicate_offset() { > + next_test; cleanup > + if [ ! -e "${TLOB_MONITOR}" ]; then > + tap_skip "uprobe_duplicate_offset" "monitor file not > available" > + cleanup; return > + fi > + if [ ! -x "${UPROBE_TARGET}" ]; then > + tap_skip "uprobe_duplicate_offset" \ > + "tlob_uprobe_target not found or not executable" > + cleanup; return > + fi > + > + busy_offset=3D$(get_uprobe_offset "${UPROBE_TARGET}" "tlob_busy_work") > + stop_offset=3D$(get_uprobe_offset "${UPROBE_TARGET}" > "tlob_busy_work_done") > + if [ -z "${busy_offset}" ] || [ -z "${stop_offset}" ]; then > + tap_skip "uprobe_duplicate_offset" \ > + "cannot resolve tlob_busy_work/tlob_busy_work_done > offset" > + cleanup; return > + fi > + > + tlob_enable > + > + # First binding: should succeed > + if ! echo "5000000:${busy_offset}:${stop_offset}:${UPROBE_TARGET}" \ > + =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 > "${TLOB_MONITOR}" 2>/dev/n= ull; then > + tap_skip "uprobe_duplicate_offset" \ > + "uprobe binding rejected (CONFIG_UPROBES=3Dy needed)" > + cleanup; return > + fi > + > + # Second binding with same offset_start: must be rejected > + if echo "9999:${busy_offset}:${stop_offset}:${UPROBE_TARGET}" \ > + =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 > "${TLOB_MONITOR}" 2>/dev/n= ull; then > + echo "-${busy_offset}:${UPROBE_TARGET}" > "${TLOB_MONITOR}" > 2>/dev/null > + tap_fail "uprobe_duplicate_offset" \ > + "duplicate offset_start was accepted (expected > error)" > + cleanup; return > + fi > + > + echo "-${busy_offset}:${UPROBE_TARGET}" > "${TLOB_MONITOR}" > 2>/dev/null > + tap_pass "uprobe_duplicate_offset" > + cleanup > +} > + > + > +# > +# Region A: tlob_busy_work with a 5 s budget - should NOT fire during th= e > test. > +# Region B: tlob_busy_work_done with a 10 us budget - SHOULD fire quickl= y > since > +#=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 tlob_uprob= e_target calls tlob_busy_work_done after a busy spin. > +# > +# Verifies that independent bindings for different offsets in the same b= inary > +# are tracked separately and that only the tight-budget binding triggers= a > +# budget_exceeded event. > +# ----------------------------------------------------------------------= ----- > +run_test_uprobe_independent_thresholds() { > + next_test; cleanup > + if [ ! -e "${TLOB_MONITOR}" ]; then > + tap_skip "uprobe_independent_thresholds" \ > + "monitor file not available"; cleanup; return > + fi > + if [ ! -x "${UPROBE_TARGET}" ]; then > + tap_skip "uprobe_independent_thresholds" \ > + "tlob_uprobe_target not found or not executable" > + cleanup; return > + fi > + > + busy_offset=3D$(get_uprobe_offset "${UPROBE_TARGET}" "tlob_busy_work") > + busy_stop_offset=3D$(get_uprobe_offset "${UPROBE_TARGET}" > "tlob_busy_work_done") > + if [ -z "${busy_offset}" ] || [ -z "${busy_stop_offset}" ]; then > + tap_skip "uprobe_independent_thresholds" \ > + "cannot resolve tlob_busy_work/tlob_busy_work_done > offset" > + cleanup; return > + fi > + > + "${UPROBE_TARGET}" 30000 & > + busy_pid=3D$! > + sleep 0.05 > + > + trace_event_enable > + trace_on > + tlob_enable > + trace_clear > + > + # Region A: generous 5 s budget on tlob_busy_work entry (should not > fire) > + if ! echo > "5000000:${busy_offset}:${busy_stop_offset}:${UPROBE_TARGET}" \ > + =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 > "${TLOB_MONITOR}" 2>/dev/n= ull; then > + kill "${busy_pid}" 2>/dev/null; wait "${busy_pid}" > 2>/dev/null > + tap_skip "uprobe_independent_thresholds" \ > + "uprobe binding rejected (CONFIG_UPROBES=3Dy needed)" > + cleanup; return > + fi > + # Region B: tight 10 us budget on tlob_busy_work_done (fires quickly) > + echo "10:${busy_stop_offset}:${busy_stop_offset}:${UPROBE_TARGET}" \ > + > "${TLOB_MONITOR}" 2>/dev/null > + > + found=3D0; i=3D0 > + while [ "$i" -lt 20 ]; do > + sleep 0.1 > + trace_grep "budget_exceeded" && { found=3D1; break; } > + i=3D$((i+1)) > + done > + > + echo "-${busy_offset}:${UPROBE_TARGET}" > "${TLOB_MONITOR}" > 2>/dev/null > + echo "-${busy_stop_offset}:${UPROBE_TARGET}" > "${TLOB_MONITOR}" > 2>/dev/null > + kill "${busy_pid}" 2>/dev/null; wait "${busy_pid}" 2>/dev/null > + > + if [ "${found}" !=3D "1" ]; then > + tap_fail "uprobe_independent_thresholds" \ > + "budget_exceeded not raised for tight-budget region > within 2 s" > + cleanup; return > + fi > + > + # The violation must carry threshold=3D10 (Region B's budget). > + ev=3D$(grep "budget_exceeded" "${TRACE_FILE}" | head -n 1) > + if ! echo "${ev}" | grep -q "threshold=3D10 "; then > + tap_fail "uprobe_independent_thresholds" \ > + "violation threshold is not Region B's 10 us: ${ev}" > + cleanup; return > + fi > + tap_pass "uprobe_independent_thresholds" > + cleanup > +} > + > +# ----------------------------------------------------------------------= ----- > +# ioctl tests via tlob_helper > +# > +# Each test invokes the helper with a sub-test name. > +# Exit code: 0=3Dpass, 1=3Dfail, 2=3Dskip. > +# ----------------------------------------------------------------------= ----- > +run_ioctl_test() { > + testname=3D$1 > + next_test > + > + if [ ! -x "${IOCTL_HELPER}" ]; then > + tap_skip "ioctl_${testname}" \ > + "tlob_helper not found or not executable" > + return > + fi > + if [ ! -c "${RV_DEV}" ]; then > + tap_skip "ioctl_${testname}" \ > + "${RV_DEV} not present (CONFIG_RV_CHARDEV=3Dy needed)" > + return > + fi > + > + tlob_enable > + "${IOCTL_HELPER}" "${testname}" > + rc=3D$? > + tlob_disable > + > + case "${rc}" in > + 0) tap_pass "ioctl_${testname}" ;; > + 2) tap_skip "ioctl_${testname}" "helper returned skip" ;; > + *) tap_fail "ioctl_${testname}" "helper exited with code ${rc}" ;; > + esac > +} > + > +# run_ioctl_test_not_enabled - like run_ioctl_test but deliberately does= NOT > +# enable the tlob monitor before invoking the helper.=C2=A0 Used to veri= fy that > +# ioctls issued against a disabled monitor return ENODEV rather than cra= shing > +# the kernel with a NULL pointer dereference. > +run_ioctl_test_not_enabled() > +{ > + next_test > + > + if [ ! -x "${IOCTL_HELPER}" ]; then > + tap_skip "ioctl_not_enabled" \ > + "tlob_helper not found or not executable" > + return > + fi > + if [ ! -c "${RV_DEV}" ]; then > + tap_skip "ioctl_not_enabled" \ > + "${RV_DEV} not present (CONFIG_RV_CHARDEV=3Dy needed)" > + return > + fi > + > + # Monitor intentionally left disabled. > + tlob_disable > + "${IOCTL_HELPER}" not_enabled > + rc=3D$? > + > + case "${rc}" in > + 0) tap_pass "ioctl_not_enabled" ;; > + 2) tap_skip "ioctl_not_enabled" "helper returned skip" ;; > + *) tap_fail "ioctl_not_enabled" "helper exited with code ${rc}" ;; > + esac > +} > + > +# ----------------------------------------------------------------------= ----- > +# Main > +# ----------------------------------------------------------------------= ----- > +check_root; check_tracefs; check_rv_dir; check_tlob > +tap_header; tap_plan 20 > + > +# tracefs interface tests > +run_test_enable_disable > +run_test_tracefs_files > + > +# uprobe external monitoring tests > +run_test_uprobe_no_false_positive > +run_test_uprobe_violation > +run_test_uprobe_unbind > +run_test_uprobe_duplicate_offset > +run_test_uprobe_independent_thresholds > + > +# /dev/rv ioctl self-instrumentation tests > +run_ioctl_test_not_enabled > +run_ioctl_test within_budget > +run_ioctl_test over_budget_cpu > +run_ioctl_test over_budget_sleep > +run_ioctl_test double_start > +run_ioctl_test stop_no_start > +run_ioctl_test multi_thread > +run_ioctl_test self_watch > +run_ioctl_test invalid_flags > +run_ioctl_test notify_fd_bad > +run_ioctl_test mmap_basic > +run_ioctl_test mmap_errors > +run_ioctl_test mmap_consume > + > +echo "# Passed: ${t_pass} Failed: ${t_fail} Skipped: ${t_skip}" > +[ "${t_fail}" -gt 0 ] && exit 1 || exit 0 > diff --git a/tools/testing/selftests/rv/tlob_helper.c > b/tools/testing/selftests/rv/tlob_helper.c > new file mode 100644 > index 000000000..cd76b56d1 > --- /dev/null > +++ b/tools/testing/selftests/rv/tlob_helper.c > @@ -0,0 +1,994 @@ > +// SPDX-License-Identifier: GPL-2.0 > +/* > + * tlob_helper.c - test helper and ELF utility for tlob selftests > + * > + * Called by test_tlob.sh to exercise the /dev/rv ioctl interface and to > + * resolve ELF symbol offsets for uprobe bindings.=C2=A0 One subcommand = per > + * invocation so the shell script can report each as an independent TAP > + * test case. > + * > + * Usage: tlob_helper [args...] > + * > + * Synchronous TRACE_START / TRACE_STOP tests: > + *=C2=A0=C2=A0 not_enabled=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - T= RACE_START without tlob enabled -> ENODEV (no > kernel crash) > + *=C2=A0=C2=A0 within_budget=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - start(50000= us), sleep 10 ms, stop -> expect 0 > + *=C2=A0=C2=A0 over_budget_cpu=C2=A0=C2=A0=C2=A0 - start(5000 us), busys= pin 100 ms, stop -> EOVERFLOW > + *=C2=A0=C2=A0 over_budget_sleep=C2=A0 - start(3000 us), sleep 50 ms, st= op -> EOVERFLOW > + * > + * Error-handling tests: > + *=C2=A0=C2=A0 double_start=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - two st= arts without stop -> EEXIST on second > + *=C2=A0=C2=A0 stop_no_start=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - stop withou= t start -> ESRCH > + * > + * Per-thread isolation test: > + *=C2=A0=C2=A0 multi_thread=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - two th= reads share one fd; one within budget, one > over > + * > + * Asynchronous notification test (notify_fd + read()): > + *=C2=A0=C2=A0 self_watch=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 - one worker exceeds budget; monitor fd receives one > ntf via read() > + * > + * Input-validation tests (TRACE_START error paths): > + *=C2=A0=C2=A0 invalid_flags=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - TRACE_START= with flags !=3D 0 -> EINVAL > + *=C2=A0=C2=A0 notify_fd_bad=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - TRACE_START= with notify_fd =3D stdout (non-rv fd) -> > EINVAL > + * > + * mmap ring buffer tests (Scenario D): > + *=C2=A0=C2=A0 mmap_basic=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0 - mmap succeeds; verify tlob_mmap_page fields > + *=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (vers= ion, capacity, data_offset, record_size) > + *=C2=A0=C2=A0 mmap_errors=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - M= AP_PRIVATE, wrong size, and non-zero pgoff all > + *=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 retur= n EINVAL > + *=C2=A0=C2=A0 mmap_consume=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - trigge= r a real violation via self-notification and > + *=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 consu= me the event through the mmap'd ring > + * > + * ELF utility (does not require /dev/rv): > + *=C2=A0=C2=A0 sym_offset > + *=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - print the ELF f= ile offset of in > + *=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (used= by the shell script to build uprobe bindings) > + * > + * Exit code: 0 =3D pass, 1 =3D fail, 2 =3D skip (device not available). > + */ > +#define _GNU_SOURCE > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > + > +#include > + > +/* Default ring capacity allocated at open(); matches TLOB_RING_DEFAULT_= CAP. > */ > +#define TLOB_RING_DEFAULT_CAP 64U > + > +static int rv_fd =3D -1; > + > +static int open_rv(void) > +{ > + rv_fd =3D open("/dev/rv", O_RDWR); > + if (rv_fd < 0) { > + fprintf(stderr, "open /dev/rv: %s\n", strerror(errno)); > + return -1; > + } > + return 0; > +} > + > +static void busy_spin_us(unsigned long us) > +{ > + struct timespec start, now; > + unsigned long elapsed; > + > + clock_gettime(CLOCK_MONOTONIC, &start); > + do { > + clock_gettime(CLOCK_MONOTONIC, &now); > + elapsed =3D (unsigned long)(now.tv_sec - start.tv_sec) > + =C2=A0 * 1000000000UL > + + (unsigned long)(now.tv_nsec - start.tv_nsec); > + } while (elapsed < us * 1000UL); > +} > + > +static int do_start(uint64_t threshold_us) > +{ > + struct tlob_start_args args =3D { > + .threshold_us =3D threshold_us, > + .notify_fd=C2=A0=C2=A0=C2=A0 =3D -1, > + }; > + > + return ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args); > +} > + > +static int do_stop(void) > +{ > + return ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL); > +} > + > +/* ---------------------------------------------------------------------= -- > + * Synchronous TRACE_START / TRACE_STOP tests > + * ---------------------------------------------------------------------= -- > + */ > + > +/* > + * test_not_enabled - TRACE_START must return ENODEV when the tlob monit= or > + * has not been enabled (tlob_state_cache is NULL). > + * > + * The shell wrapper deliberately does NOT call tlob_enable before invok= ing > + * this subcommand, so the ioctl is expected to fail with ENODEV rather = than > + * crashing the kernel with a NULL pointer dereference in kmem_cache_all= oc. > + */ > +static int test_not_enabled(void) > +{ > + int ret; > + > + ret =3D do_start(1000); > + if (ret =3D=3D 0) { > + fprintf(stderr, "TRACE_START: expected ENODEV, got > success\n"); > + do_stop(); > + return 1; > + } > + if (errno !=3D ENODEV) { > + fprintf(stderr, "TRACE_START: expected ENODEV, got %s\n", > + strerror(errno)); > + return 1; > + } > + return 0; > +} > + > +static int test_within_budget(void) > +{ > + int ret; > + > + if (do_start(50000) < 0) { > + fprintf(stderr, "TRACE_START: %s\n", strerror(errno)); > + return 1; > + } > + usleep(10000); /* 10 ms < 50 ms budget */ > + ret =3D do_stop(); > + if (ret !=3D 0) { > + fprintf(stderr, "TRACE_STOP: expected 0, got %d errno=3D%s\n", > + ret, strerror(errno)); > + return 1; > + } > + return 0; > +} > + > +static int test_over_budget_cpu(void) > +{ > + int ret; > + > + if (do_start(5000) < 0) { > + fprintf(stderr, "TRACE_START: %s\n", strerror(errno)); > + return 1; > + } > + busy_spin_us(100000); /* 100 ms >> 5 ms budget */ > + ret =3D do_stop(); > + if (ret =3D=3D 0) { > + fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got 0\n"); > + return 1; > + } > + if (errno !=3D EOVERFLOW) { > + fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got %s\n", > + strerror(errno)); > + return 1; > + } > + return 0; > +} > + > +static int test_over_budget_sleep(void) > +{ > + int ret; > + > + if (do_start(3000) < 0) { > + fprintf(stderr, "TRACE_START: %s\n", strerror(errno)); > + return 1; > + } > + usleep(50000); /* 50 ms >> 3 ms budget, off-CPU time counts */ > + ret =3D do_stop(); > + if (ret =3D=3D 0) { > + fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got 0\n"); > + return 1; > + } > + if (errno !=3D EOVERFLOW) { > + fprintf(stderr, "TRACE_STOP: expected EOVERFLOW, got %s\n", > + strerror(errno)); > + return 1; > + } > + return 0; > +} > + > +/* ---------------------------------------------------------------------= -- > + * Error-handling tests > + * ---------------------------------------------------------------------= -- > + */ > + > +static int test_double_start(void) > +{ > + int ret; > + > + if (do_start(10000000) < 0) { > + fprintf(stderr, "first TRACE_START: %s\n", strerror(errno)); > + return 1; > + } > + ret =3D do_start(10000000); > + if (ret =3D=3D 0) { > + fprintf(stderr, "second TRACE_START: expected EEXIST, got > 0\n"); > + do_stop(); > + return 1; > + } > + if (errno !=3D EEXIST) { > + fprintf(stderr, "second TRACE_START: expected EEXIST, got > %s\n", > + strerror(errno)); > + do_stop(); > + return 1; > + } > + do_stop(); /* clean up */ > + return 0; > +} > + > +static int test_stop_no_start(void) > +{ > + int ret; > + > + /* Ensure clean state: ignore error from a stale entry */ > + do_stop(); > + > + ret =3D do_stop(); > + if (ret =3D=3D 0) { > + fprintf(stderr, "TRACE_STOP: expected ESRCH, got 0\n"); > + return 1; > + } > + if (errno !=3D ESRCH) { > + fprintf(stderr, "TRACE_STOP: expected ESRCH, got %s\n", > + strerror(errno)); > + return 1; > + } > + return 0; > +} > + > +/* ---------------------------------------------------------------------= -- > + * Per-thread isolation test > + * > + * Two threads share a single /dev/rv fd.=C2=A0 The monitor uses task_st= ruct * > + * as the key, so each thread gets an independent slot regardless of the > + * shared fd. > + * ---------------------------------------------------------------------= -- > + */ > + > +struct mt_thread_args { > + uint64_t=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 threshold_us; > + unsigned long workload_us; > + int=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 busy; > + int=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 expect_= eoverflow; > + int=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 result; > +}; > + > +static void *mt_thread_fn(void *arg) > +{ > + struct mt_thread_args *a =3D arg; > + int ret; > + > + if (do_start(a->threshold_us) < 0) { > + fprintf(stderr, "thread TRACE_START: %s\n", strerror(errno)); > + a->result =3D 1; > + return NULL; > + } > + > + if (a->busy) > + busy_spin_us(a->workload_us); > + else > + usleep(a->workload_us); > + > + ret =3D do_stop(); > + if (a->expect_eoverflow) { > + if (ret =3D=3D 0 || errno !=3D EOVERFLOW) { > + fprintf(stderr, "thread: expected EOVERFLOW, got > ret=3D%d errno=3D%s\n", > + ret, strerror(errno)); > + a->result =3D 1; > + return NULL; > + } > + } else { > + if (ret !=3D 0) { > + fprintf(stderr, "thread: expected 0, got ret=3D%d > errno=3D%s\n", > + ret, strerror(errno)); > + a->result =3D 1; > + return NULL; > + } > + } > + a->result =3D 0; > + return NULL; > +} > + > +static int test_multi_thread(void) > +{ > + pthread_t ta, tb; > + struct mt_thread_args a =3D { > + .threshold_us=C2=A0=C2=A0=C2=A0=C2=A0 =3D 20000,=C2=A0 /* 20 ms */ > + .workload_us=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D 5000,=C2=A0=C2=A0 /* 5 = ms sleep -> within budget */ > + .busy=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0 =3D 0, > + .expect_eoverflow =3D 0, > + }; > + struct mt_thread_args b =3D { > + .threshold_us=C2=A0=C2=A0=C2=A0=C2=A0 =3D 3000,=C2=A0=C2=A0 /* 3 ms */ > + .workload_us=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D 30000,=C2=A0 /* 30 ms s= pin -> over budget */ > + .busy=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0 =3D 1, > + .expect_eoverflow =3D 1, > + }; > + > + pthread_create(&ta, NULL, mt_thread_fn, &a); > + pthread_create(&tb, NULL, mt_thread_fn, &b); > + pthread_join(ta, NULL); > + pthread_join(tb, NULL); > + > + return (a.result || b.result) ? 1 : 0; > +} > + > +/* ---------------------------------------------------------------------= -- > + * Asynchronous notification test (notify_fd + read()) > + * > + * A dedicated monitor_fd is opened by the main thread.=C2=A0 Two worker= threads > + * each open their own work_fd and call TLOB_IOCTL_TRACE_START with > + * notify_fd =3D monitor_fd, nominating it as the violation target.=C2= =A0 Worker A > + * stays within budget; worker B exceeds it.=C2=A0 The main thread reads= from > + * monitor_fd and expects exactly one tlob_event record. > + * ---------------------------------------------------------------------= -- > + */ > + > +struct sw_worker_args { > + int=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 monitor= _fd; > + uint64_t=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 threshold_us; > + unsigned long workload_us; > + int=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 busy; > + int=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 result; > +}; > + > +static void *sw_worker_fn(void *arg) > +{ > + struct sw_worker_args *a =3D arg; > + struct tlob_start_args args =3D { > + .threshold_us =3D a->threshold_us, > + .notify_fd=C2=A0=C2=A0=C2=A0 =3D a->monitor_fd, > + }; > + int work_fd; > + int ret; > + > + work_fd =3D open("/dev/rv", O_RDWR); > + if (work_fd < 0) { > + fprintf(stderr, "worker open /dev/rv: %s\n", > strerror(errno)); > + a->result =3D 1; > + return NULL; > + } > + > + ret =3D ioctl(work_fd, TLOB_IOCTL_TRACE_START, &args); > + if (ret < 0) { > + fprintf(stderr, "TRACE_START (notify): %s\n", > strerror(errno)); > + close(work_fd); > + a->result =3D 1; > + return NULL; > + } > + > + if (a->busy) > + busy_spin_us(a->workload_us); > + else > + usleep(a->workload_us); > + > + ioctl(work_fd, TLOB_IOCTL_TRACE_STOP, NULL); > + close(work_fd); > + a->result =3D 0; > + return NULL; > +} > + > +static int test_self_watch(void) > +{ > + int monitor_fd; > + pthread_t ta, tb; > + struct sw_worker_args a =3D { > + .threshold_us =3D 50000,=C2=A0 /* 50 ms */ > + .workload_us=C2=A0 =3D 5000,=C2=A0=C2=A0 /* 5 ms sleep -> no violation= */ > + .busy=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D 0, > + }; > + struct sw_worker_args b =3D { > + .threshold_us =3D 3000,=C2=A0=C2=A0 /* 3 ms */ > + .workload_us=C2=A0 =3D 30000,=C2=A0 /* 30 ms spin -> violation */ > + .busy=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D 1, > + }; > + struct tlob_event ntfs[8]; > + int violations =3D 0; > + ssize_t n; > + > + /* > + * Open monitor_fd with O_NONBLOCK so read() after the workers finish > + * returns immediately rather than blocking forever. > + */ > + monitor_fd =3D open("/dev/rv", O_RDWR | O_NONBLOCK); > + if (monitor_fd < 0) { > + fprintf(stderr, "open /dev/rv (monitor_fd): %s\n", > strerror(errno)); > + return 1; > + } > + a.monitor_fd =3D monitor_fd; > + b.monitor_fd =3D monitor_fd; > + > + pthread_create(&ta, NULL, sw_worker_fn, &a); > + pthread_create(&tb, NULL, sw_worker_fn, &b); > + pthread_join(ta, NULL); > + pthread_join(tb, NULL); > + > + if (a.result || b.result) { > + close(monitor_fd); > + return 1; > + } > + > + /* > + * Drain all available tlob_event records.=C2=A0 With O_NONBLOCK the fi= nal > + * read() returns -EAGAIN when the buffer is empty. > + */ > + while ((n =3D read(monitor_fd, ntfs, sizeof(ntfs))) > 0) > + violations +=3D (int)(n / sizeof(struct tlob_event)); > + > + close(monitor_fd); > + > + if (violations !=3D 1) { > + fprintf(stderr, "self_watch: expected 1 violation, got %d\n", > + violations); > + return 1; > + } > + return 0; > +} > + > +/* ---------------------------------------------------------------------= -- > + * Input-validation tests (TRACE_START error paths) > + * ---------------------------------------------------------------------= -- > + */ > + > +/* > + * test_invalid_flags - TRACE_START with flags !=3D 0 must return EINVAL= . > + * > + * The flags field is reserved for future extensions and must be zero. > + * Callers that set it to a non-zero value are rejected early so that a > + * future kernel can assign meaning to those bits without silently > + * ignoring them. > + */ > +static int test_invalid_flags(void) > +{ > + struct tlob_start_args args =3D { > + .threshold_us =3D 1000, > + .notify_fd=C2=A0=C2=A0=C2=A0 =3D -1, > + .flags=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D 1,=C2=A0=C2=A0 /*= non-zero: must be rejected */ > + }; > + int ret; > + > + ret =3D ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args); > + if (ret =3D=3D 0) { > + fprintf(stderr, "TRACE_START(flags=3D1): expected EINVAL, got > success\n"); > + do_stop(); > + return 1; > + } > + if (errno !=3D EINVAL) { > + fprintf(stderr, "TRACE_START(flags=3D1): expected EINVAL, got > %s\n", > + strerror(errno)); > + return 1; > + } > + return 0; > +} > + > +/* > + * test_notify_fd_bad - TRACE_START with a non-/dev/rv notify_fd must re= turn > + * EINVAL. > + * > + * When notify_fd >=3D 0, the kernel resolves it to a struct file and ch= ecks > + * that its private_data is non-NULL (i.e. it is a /dev/rv file descript= or). > + * Passing stdout (fd 1) supplies a real, open fd whose private_data is = NULL, > + * so the kernel must reject it with EINVAL. > + */ > +static int test_notify_fd_bad(void) > +{ > + struct tlob_start_args args =3D { > + .threshold_us =3D 1000, > + .notify_fd=C2=A0=C2=A0=C2=A0 =3D STDOUT_FILENO,=C2=A0=C2=A0 /* open bu= t not a /dev/rv fd > */ > + .flags=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D 0, > + }; > + int ret; > + > + ret =3D ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args); > + if (ret =3D=3D 0) { > + fprintf(stderr, > + "TRACE_START(notify_fd=3Dstdout): expected EINVAL, got > success\n"); > + do_stop(); > + return 1; > + } > + if (errno !=3D EINVAL) { > + fprintf(stderr, > + "TRACE_START(notify_fd=3Dstdout): expected EINVAL, got > %s\n", > + strerror(errno)); > + return 1; > + } > + return 0; > +} > + > +/* ---------------------------------------------------------------------= -- > + * mmap ring buffer tests (Scenario D) > + * ---------------------------------------------------------------------= -- > + */ > + > +/* > + * test_mmap_basic - mmap the ring buffer and verify the control page fi= elds. > + * > + * The kernel allocates TLOB_RING_DEFAULT_CAP records at open().=C2=A0 A= shared > + * mmap of PAGE_SIZE + cap * record_size must succeed and the tlob_mmap_= page > + * header must contain consistent values. > + */ > +static int test_mmap_basic(void) > +{ > + long pagesize =3D sysconf(_SC_PAGESIZE); > + size_t mmap_len =3D (size_t)pagesize + > + =C2=A0 TLOB_RING_DEFAULT_CAP * sizeof(struct tlob_event); > + /* rv_mmap requires a page-aligned length */ > + mmap_len =3D (mmap_len + (size_t)(pagesize - 1)) & ~(size_t)(pagesize - > 1); > + struct tlob_mmap_page *page; > + struct tlob_event *data; > + void *map; > + int ret =3D 0; > + > + map =3D mmap(NULL, mmap_len, PROT_READ | PROT_WRITE, MAP_SHARED, rv_fd, > 0); > + if (map =3D=3D MAP_FAILED) { > + fprintf(stderr, "mmap_basic: mmap: %s\n", strerror(errno)); > + return 1; > + } > + > + page =3D (struct tlob_mmap_page *)map; > + data =3D (struct tlob_event *)((char *)map + page->data_offset); > + > + if (page->version !=3D 1) { > + fprintf(stderr, "mmap_basic: expected version=3D1, got %u\n", > + page->version); > + ret =3D 1; > + goto out; > + } > + if (page->capacity !=3D TLOB_RING_DEFAULT_CAP) { > + fprintf(stderr, "mmap_basic: expected capacity=3D%u, got %u\n", > + TLOB_RING_DEFAULT_CAP, page->capacity); > + ret =3D 1; > + goto out; > + } > + if (page->data_offset !=3D (uint32_t)pagesize) { > + fprintf(stderr, "mmap_basic: expected data_offset=3D%ld, got > %u\n", > + pagesize, page->data_offset); > + ret =3D 1; > + goto out; > + } > + if (page->record_size !=3D sizeof(struct tlob_event)) { > + fprintf(stderr, "mmap_basic: expected record_size=3D%zu, got > %u\n", > + sizeof(struct tlob_event), page->record_size); > + ret =3D 1; > + goto out; > + } > + if (page->data_head !=3D 0 || page->data_tail !=3D 0) { > + fprintf(stderr, "mmap_basic: ring not empty at open: head=3D%u > tail=3D%u\n", > + page->data_head, page->data_tail); > + ret =3D 1; > + goto out; > + } > + /* Touch the data array to confirm it is accessible. */ > + (void)data[0].tid; > +out: > + munmap(map, mmap_len); > + return ret; > +} > + > +/* > + * test_mmap_errors - verify that rv_mmap() rejects invalid mmap paramet= ers. > + * > + * Four cases are tested, each must return MAP_FAILED with errno =3D=3D = EINVAL: > + *=C2=A0=C2=A0 1. size one page short of the correct ring length > + *=C2=A0=C2=A0 2. size one page larger than the correct ring length > + *=C2=A0=C2=A0 3. MAP_PRIVATE (only MAP_SHARED is permitted) > + *=C2=A0=C2=A0 4. non-zero vm_pgoff (offset must be 0) > + */ > +static int test_mmap_errors(void) > +{ > + long pagesize =3D sysconf(_SC_PAGESIZE); > + size_t correct_len =3D (size_t)pagesize + > + =C2=A0=C2=A0=C2=A0=C2=A0 TLOB_RING_DEFAULT_CAP * sizeof(struct > tlob_event); > + /* rv_mmap requires a page-aligned length */ > + correct_len =3D (correct_len + (size_t)(pagesize - 1)) & > ~(size_t)(pagesize - 1); > + void *map; > + int ret =3D 0; > + > + /* Case 1: size one page short (correct_len - 1 still rounds up to > correct_len) */ > + map =3D mmap(NULL, correct_len - (size_t)pagesize, PROT_READ | > PROT_WRITE, > + =C2=A0=C2=A0 MAP_SHARED, rv_fd, 0); > + if (map !=3D MAP_FAILED) { > + fprintf(stderr, "mmap_errors: short-size mmap succeeded > (expected EINVAL)\n"); > + munmap(map, correct_len - (size_t)pagesize); > + ret =3D 1; > + } else if (errno !=3D EINVAL) { > + fprintf(stderr, "mmap_errors: short-size: expected EINVAL, > got %s\n", > + strerror(errno)); > + ret =3D 1; > + } > + > + /* Case 2: size one page too large */ > + map =3D mmap(NULL, correct_len + (size_t)pagesize, PROT_READ | > PROT_WRITE, > + =C2=A0=C2=A0 MAP_SHARED, rv_fd, 0); > + if (map !=3D MAP_FAILED) { > + fprintf(stderr, "mmap_errors: oversized mmap succeeded > (expected EINVAL)\n"); > + munmap(map, correct_len + (size_t)pagesize); > + ret =3D 1; > + } else if (errno !=3D EINVAL) { > + fprintf(stderr, "mmap_errors: oversized: expected EINVAL, got > %s\n", > + strerror(errno)); > + ret =3D 1; > + } > + > + /* Case 3: MAP_PRIVATE instead of MAP_SHARED */ > + map =3D mmap(NULL, correct_len, PROT_READ | PROT_WRITE, > + =C2=A0=C2=A0 MAP_PRIVATE, rv_fd, 0); > + if (map !=3D MAP_FAILED) { > + fprintf(stderr, "mmap_errors: MAP_PRIVATE succeeded (expected > EINVAL)\n"); > + munmap(map, correct_len); > + ret =3D 1; > + } else if (errno !=3D EINVAL) { > + fprintf(stderr, "mmap_errors: MAP_PRIVATE: expected EINVAL, > got %s\n", > + strerror(errno)); > + ret =3D 1; > + } > + > + /* Case 4: non-zero file offset (pgoff =3D 1) */ > + map =3D mmap(NULL, correct_len, PROT_READ | PROT_WRITE, > + =C2=A0=C2=A0 MAP_SHARED, rv_fd, (off_t)pagesize); > + if (map !=3D MAP_FAILED) { > + fprintf(stderr, "mmap_errors: non-zero pgoff mmap succeeded > (expected EINVAL)\n"); > + munmap(map, correct_len); > + ret =3D 1; > + } else if (errno !=3D EINVAL) { > + fprintf(stderr, "mmap_errors: non-zero pgoff: expected > EINVAL, got %s\n", > + strerror(errno)); > + ret =3D 1; > + } > + > + return ret; > +} > + > +/* > + * test_mmap_consume - zero-copy consumption of a real violation event. > + * > + * Arms a 5 ms budget with self-notification (notify_fd =3D rv_fd), slee= ps > + * 50 ms (off-CPU violation), then reads the pushed event through the mm= ap'd > + * ring without calling read().=C2=A0 Verifies: > + *=C2=A0=C2=A0 - TRACE_STOP returns EOVERFLOW (budget was exceeded) > + *=C2=A0=C2=A0 - data_head =3D=3D 1 after the violation > + *=C2=A0=C2=A0 - the event fields (threshold_us, tag, tid) are correct > + *=C2=A0=C2=A0 - data_tail can be advanced to consume the record (ring e= mpties) > + */ > +static int test_mmap_consume(void) > +{ > + long pagesize =3D sysconf(_SC_PAGESIZE); > + size_t mmap_len =3D (size_t)pagesize + > + =C2=A0 TLOB_RING_DEFAULT_CAP * sizeof(struct tlob_event); > + /* rv_mmap requires a page-aligned length */ > + mmap_len =3D (mmap_len + (size_t)(pagesize - 1)) & ~(size_t)(pagesize - > 1); > + struct tlob_start_args args =3D { > + .threshold_us =3D 5000, /* 5 ms */ > + .notify_fd=C2=A0=C2=A0=C2=A0 =3D rv_fd, /* self-notification */ > + .tag=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D 0xdeadb= eefULL, > + .flags=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 =3D 0, > + }; > + struct tlob_mmap_page *page; > + struct tlob_event *data; > + void *map; > + int stop_ret; > + int ret =3D 0; > + > + map =3D mmap(NULL, mmap_len, PROT_READ | PROT_WRITE, MAP_SHARED, rv_fd, > 0); > + if (map =3D=3D MAP_FAILED) { > + fprintf(stderr, "mmap_consume: mmap: %s\n", strerror(errno)); > + return 1; > + } > + > + page =3D (struct tlob_mmap_page *)map; > + data =3D (struct tlob_event *)((char *)map + page->data_offset); > + > + if (ioctl(rv_fd, TLOB_IOCTL_TRACE_START, &args) < 0) { > + fprintf(stderr, "mmap_consume: TRACE_START: %s\n", > strerror(errno)); > + ret =3D 1; > + goto out; > + } > + > + usleep(50000); /* 50 ms >> 5 ms budget -> off-CPU violation */ > + > + stop_ret =3D ioctl(rv_fd, TLOB_IOCTL_TRACE_STOP, NULL); > + if (stop_ret =3D=3D 0) { > + fprintf(stderr, "mmap_consume: TRACE_STOP returned 0, > expected EOVERFLOW\n"); > + ret =3D 1; > + goto out; > + } > + if (errno !=3D EOVERFLOW) { > + fprintf(stderr, "mmap_consume: TRACE_STOP: expected > EOVERFLOW, got %s\n", > + strerror(errno)); > + ret =3D 1; > + goto out; > + } > + > + /* Pairs with smp_store_release in tlob_event_push. */ > + if (__atomic_load_n(&page->data_head, __ATOMIC_ACQUIRE) !=3D 1) { > + fprintf(stderr, "mmap_consume: expected data_head=3D1, got > %u\n", > + page->data_head); > + ret =3D 1; > + goto out; > + } > + if (page->data_tail !=3D 0) { > + fprintf(stderr, "mmap_consume: expected data_tail=3D0, got > %u\n", > + page->data_tail); > + ret =3D 1; > + goto out; > + } > + > + /* Verify record content */ > + if (data[0].threshold_us !=3D 5000) { > + fprintf(stderr, "mmap_consume: expected threshold_us=3D5000, > got %llu\n", > + (unsigned long long)data[0].threshold_us); > + ret =3D 1; > + goto out; > + } > + if (data[0].tag !=3D 0xdeadbeefULL) { > + fprintf(stderr, "mmap_consume: expected tag=3D0xdeadbeef, got > %llx\n", > + (unsigned long long)data[0].tag); > + ret =3D 1; > + goto out; > + } > + if (data[0].tid =3D=3D 0) { > + fprintf(stderr, "mmap_consume: tid is 0\n"); > + ret =3D 1; > + goto out; > + } > + > + /* Consume: advance data_tail and confirm ring is empty */ > + __atomic_store_n(&page->data_tail, 1U, __ATOMIC_RELEASE); > + if (__atomic_load_n(&page->data_head, __ATOMIC_ACQUIRE) !=3D > + =C2=A0=C2=A0=C2=A0 __atomic_load_n(&page->data_tail, __ATOMIC_ACQUIRE))= { > + fprintf(stderr, "mmap_consume: ring not empty after > consume\n"); > + ret =3D 1; > + } > + > +out: > + munmap(map, mmap_len); > + return ret; > +} > + > +/* ---------------------------------------------------------------------= -- > + * ELF utility: sym_offset > + * > + * Print the ELF file offset of a symbol in a binary.=C2=A0 Supports 32-= and > + * 64-bit ELF.=C2=A0 Walks the section headers to find .symtab (falling = back to > + * .dynsym), then converts the symbol's virtual address to a file offset > + * via the PT_LOAD program headers. > + * > + * Does not require /dev/rv; used by the shell script to build uprobe > + * bindings of the form > pid:threshold_us:offset_start:offset_stop:binary_path. > + * > + * Returns 0 on success (offset printed to stdout), 1 on failure. > + * ---------------------------------------------------------------------= -- > + */ > +static int sym_offset(const char *binary, const char *symname) > +{ > + int fd; > + struct stat st; > + void *map; > + Elf64_Ehdr *ehdr; > + Elf32_Ehdr *ehdr32; > + int is64; > + uint64_t sym_vaddr =3D 0; > + int found =3D 0; > + uint64_t file_offset =3D 0; > + > + fd =3D open(binary, O_RDONLY); > + if (fd < 0) { > + fprintf(stderr, "open %s: %s\n", binary, strerror(errno)); > + return 1; > + } > + if (fstat(fd, &st) < 0) { > + close(fd); > + return 1; > + } > + map =3D mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); > + close(fd); > + if (map =3D=3D MAP_FAILED) { > + fprintf(stderr, "mmap: %s\n", strerror(errno)); > + return 1; > + } > + > + /* Identify ELF class */ > + ehdr =3D (Elf64_Ehdr *)map; > + ehdr32 =3D (Elf32_Ehdr *)map; > + if (st.st_size < 4 || > + =C2=A0=C2=A0=C2=A0 ehdr->e_ident[EI_MAG0] !=3D ELFMAG0 || > + =C2=A0=C2=A0=C2=A0 ehdr->e_ident[EI_MAG1] !=3D ELFMAG1 || > + =C2=A0=C2=A0=C2=A0 ehdr->e_ident[EI_MAG2] !=3D ELFMAG2 || > + =C2=A0=C2=A0=C2=A0 ehdr->e_ident[EI_MAG3] !=3D ELFMAG3) { > + fprintf(stderr, "%s: not an ELF file\n", binary); > + munmap(map, (size_t)st.st_size); > + return 1; > + } > + is64 =3D (ehdr->e_ident[EI_CLASS] =3D=3D ELFCLASS64); > + > + if (is64) { > + /* Walk section headers to find .symtab or .dynsym */ > + Elf64_Shdr *shdrs =3D (Elf64_Shdr *)((char *)map + ehdr- > >e_shoff); > + Elf64_Shdr *shstrtab_hdr =3D &shdrs[ehdr->e_shstrndx]; > + const char *shstrtab =3D (char *)map + shstrtab_hdr->sh_offset; > + int si; > + > + /* Prefer .symtab; fall back to .dynsym */ > + for (int pass =3D 0; pass < 2 && !found; pass++) { > + const char *target =3D pass ? ".dynsym" : ".symtab"; > + > + for (si =3D 0; si < ehdr->e_shnum && !found; si++) { > + Elf64_Shdr *sh =3D &shdrs[si]; > + const char *name =3D shstrtab + sh->sh_name; > + > + if (strcmp(name, target) !=3D 0) > + continue; > + > + Elf64_Shdr *strtab_sh =3D &shdrs[sh->sh_link]; > + const char *strtab =3D (char *)map + strtab_sh- > >sh_offset; > + Elf64_Sym *syms =3D (Elf64_Sym *)((char *)map + > sh->sh_offset); > + uint64_t nsyms =3D sh->sh_size / > sizeof(Elf64_Sym); > + uint64_t j; > + > + for (j =3D 0; j < nsyms; j++) { > + if (strcmp(strtab + syms[j].st_name, > symname) =3D=3D 0) { > + sym_vaddr =3D syms[j].st_value; > + found =3D 1; > + break; > + } > + } > + } > + } > + > + if (!found) { > + fprintf(stderr, "symbol '%s' not found in %s\n", > symname, binary); > + munmap(map, (size_t)st.st_size); > + return 1; > + } > + > + /* Convert vaddr to file offset via PT_LOAD segments */ > + Elf64_Phdr *phdrs =3D (Elf64_Phdr *)((char *)map + ehdr- > >e_phoff); > + int pi; > + > + for (pi =3D 0; pi < ehdr->e_phnum; pi++) { > + Elf64_Phdr *ph =3D &phdrs[pi]; > + > + if (ph->p_type !=3D PT_LOAD) > + continue; > + if (sym_vaddr >=3D ph->p_vaddr && > + =C2=A0=C2=A0=C2=A0 sym_vaddr < ph->p_vaddr + ph->p_filesz) { > + file_offset =3D sym_vaddr - ph->p_vaddr + ph- > >p_offset; > + break; > + } > + } > + } else { > + /* 32-bit ELF */ > + Elf32_Shdr *shdrs =3D (Elf32_Shdr *)((char *)map + ehdr32- > >e_shoff); > + Elf32_Shdr *shstrtab_hdr =3D &shdrs[ehdr32->e_shstrndx]; > + const char *shstrtab =3D (char *)map + shstrtab_hdr->sh_offset; > + int si; > + uint32_t sym_vaddr32 =3D 0; > + > + for (int pass =3D 0; pass < 2 && !found; pass++) { > + const char *target =3D pass ? ".dynsym" : ".symtab"; > + > + for (si =3D 0; si < ehdr32->e_shnum && !found; si++) { > + Elf32_Shdr *sh =3D &shdrs[si]; > + const char *name =3D shstrtab + sh->sh_name; > + > + if (strcmp(name, target) !=3D 0) > + continue; > + > + Elf32_Shdr *strtab_sh =3D &shdrs[sh->sh_link]; > + const char *strtab =3D (char *)map + strtab_sh- > >sh_offset; > + Elf32_Sym *syms =3D (Elf32_Sym *)((char *)map + > sh->sh_offset); > + uint32_t nsyms =3D sh->sh_size / > sizeof(Elf32_Sym); > + uint32_t j; > + > + for (j =3D 0; j < nsyms; j++) { > + if (strcmp(strtab + syms[j].st_name, > symname) =3D=3D 0) { > + sym_vaddr32 =3D > syms[j].st_value; > + found =3D 1; > + break; > + } > + } > + } > + } > + > + if (!found) { > + fprintf(stderr, "symbol '%s' not found in %s\n", > symname, binary); > + munmap(map, (size_t)st.st_size); > + return 1; > + } > + > + Elf32_Phdr *phdrs =3D (Elf32_Phdr *)((char *)map + ehdr32- > >e_phoff); > + int pi; > + > + for (pi =3D 0; pi < ehdr32->e_phnum; pi++) { > + Elf32_Phdr *ph =3D &phdrs[pi]; > + > + if (ph->p_type !=3D PT_LOAD) > + continue; > + if (sym_vaddr32 >=3D ph->p_vaddr && > + =C2=A0=C2=A0=C2=A0 sym_vaddr32 < ph->p_vaddr + ph->p_filesz) { > + file_offset =3D sym_vaddr32 - ph->p_vaddr + ph- > >p_offset; > + break; > + } > + } > + sym_vaddr =3D sym_vaddr32; > + } > + > + munmap(map, (size_t)st.st_size); > + > + if (!file_offset && sym_vaddr) { > + fprintf(stderr, "could not map vaddr 0x%lx to file offset\n", > + (unsigned long)sym_vaddr); > + return 1; > + } > + > + printf("0x%lx\n", (unsigned long)file_offset); > + return 0; > +} > + > +int main(int argc, char *argv[]) > +{ > + int rc; > + > + if (argc < 2) { > + fprintf(stderr, "Usage: %s [args...]\n", > argv[0]); > + return 1; > + } > + > + /* sym_offset does not need /dev/rv */ > + if (strcmp(argv[1], "sym_offset") =3D=3D 0) { > + if (argc < 4) { > + fprintf(stderr, "Usage: %s sym_offset > \n", > + argv[0]); > + return 1; > + } > + return sym_offset(argv[2], argv[3]); > + } > + > + if (open_rv() < 0) > + return 2; /* skip */ > + > + if (strcmp(argv[1], "not_enabled") =3D=3D 0) > + rc =3D test_not_enabled(); > + else if (strcmp(argv[1], "within_budget") =3D=3D 0) > + rc =3D test_within_budget(); > + else if (strcmp(argv[1], "over_budget_cpu") =3D=3D 0) > + rc =3D test_over_budget_cpu(); > + else if (strcmp(argv[1], "over_budget_sleep") =3D=3D 0) > + rc =3D test_over_budget_sleep(); > + else if (strcmp(argv[1], "double_start") =3D=3D 0) > + rc =3D test_double_start(); > + else if (strcmp(argv[1], "stop_no_start") =3D=3D 0) > + rc =3D test_stop_no_start(); > + else if (strcmp(argv[1], "multi_thread") =3D=3D 0) > + rc =3D test_multi_thread(); > + else if (strcmp(argv[1], "self_watch") =3D=3D 0) > + rc =3D test_self_watch(); > + else if (strcmp(argv[1], "invalid_flags") =3D=3D 0) > + rc =3D test_invalid_flags(); > + else if (strcmp(argv[1], "notify_fd_bad") =3D=3D 0) > + rc =3D test_notify_fd_bad(); > + else if (strcmp(argv[1], "mmap_basic") =3D=3D 0) > + rc =3D test_mmap_basic(); > + else if (strcmp(argv[1], "mmap_errors") =3D=3D 0) > + rc =3D test_mmap_errors(); > + else if (strcmp(argv[1], "mmap_consume") =3D=3D 0) > + rc =3D test_mmap_consume(); > + else { > + fprintf(stderr, "Unknown test: %s\n", argv[1]); > + rc =3D 1; > + } > + > + close(rv_fd); > + return rc; > +} > diff --git a/tools/testing/selftests/rv/tlob_uprobe_target.c > b/tools/testing/selftests/rv/tlob_uprobe_target.c > new file mode 100644 > index 000000000..6c895cb40 > --- /dev/null > +++ b/tools/testing/selftests/rv/tlob_uprobe_target.c > @@ -0,0 +1,108 @@ > +// SPDX-License-Identifier: GPL-2.0 > +/* > + * tlob_uprobe_target.c - uprobe target binary for tlob selftests. > + * > + * Provides two well-known probe points: > + *=C2=A0=C2=A0 tlob_busy_work()=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 - start pr= obe: arms the tlob budget timer > + *=C2=A0=C2=A0 tlob_busy_work_done() - stop=C2=A0 probe: cancels the tim= er on completion > + * > + * The tlob selftest writes a five-field uprobe binding: > + *=C2=A0=C2=A0 pid:threshold_us:binary:offset_start:offset_stop > + * where offset_start is the file offset of tlob_busy_work and offset_st= op > + * is the file offset of tlob_busy_work_done (resolved via tlob_helper > + * sym_offset). > + * > + * Both probe points are plain entry uprobes (no uretprobe).=C2=A0 The b= usy loop > + * keeps the task on-CPU so that either the stop probe fires cleanly (wi= thin > + * budget) or the hrtimer fires first and emits tlob_budget_exceeded (ov= er > + * budget). > + * > + * Usage: tlob_uprobe_target > + * > + * Loops calling tlob_busy_work() in 200 ms iterations until > + * has elapsed (0 =3D run for ~24 hours).=C2=A0 Short iterations ensure = the uprobe > + * entry fires on every call even if the uprobe is installed after the > + * program has started. > + */ > +#define _GNU_SOURCE > +#include > +#include > +#include > +#include > + > +#ifndef noinline > +#define noinline __attribute__((noinline)) > +#endif > + > +static inline int timespec_before(const struct timespec *a, > + =C2=A0=C2=A0 const struct timespec *b) > +{ > + return a->tv_sec < b->tv_sec || > + =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 (a->tv_sec =3D=3D b->tv_sec && a->= tv_nsec < b->tv_nsec); > +} > + > +static void timespec_add_ms(struct timespec *ts, unsigned long ms) > +{ > + ts->tv_sec=C2=A0 +=3D ms / 1000; > + ts->tv_nsec +=3D (long)(ms % 1000) * 1000000L; > + if (ts->tv_nsec >=3D 1000000000L) { > + ts->tv_sec++; > + ts->tv_nsec -=3D 1000000000L; > + } > +} > + > +/* > + * tlob_busy_work_done - stop-probe target. > + * > + * Called by tlob_busy_work() after the busy loop.=C2=A0 The uprobe on t= his > + * function's entry fires tlob_stop_task(), cancelling the budget timer. > + * noinline ensures the compiler never merges this function with its cal= ler, > + * guaranteeing the entry uprobe always fires. > + */ > +noinline void tlob_busy_work_done(void) > +{ > + /* empty: the uprobe fires on entry */ > +} > + > +/* > + * tlob_busy_work - start-probe target. > + * > + * The uprobe on this function's entry fires tlob_start_task(), arming t= he > + * budget timer.=C2=A0 noinline prevents the compiler and linker (includ= ing LTO) > + * from inlining this function into its callers, ensuring the entry upro= be > + * fires on every call. > + */ > +noinline void tlob_busy_work(unsigned long duration_ns) > +{ > + struct timespec start, now; > + unsigned long elapsed; > + > + clock_gettime(CLOCK_MONOTONIC, &start); > + do { > + clock_gettime(CLOCK_MONOTONIC, &now); > + elapsed =3D (unsigned long)(now.tv_sec - start.tv_sec) > + =C2=A0 * 1000000000UL > + + (unsigned long)(now.tv_nsec - start.tv_nsec); > + } while (elapsed < duration_ns); > + > + tlob_busy_work_done(); > +} > + > +int main(int argc, char *argv[]) > +{ > + unsigned long duration_ms =3D 0; > + struct timespec deadline, now; > + > + if (argc >=3D 2) > + duration_ms =3D strtoul(argv[1], NULL, 10); > + > + clock_gettime(CLOCK_MONOTONIC, &deadline); > + timespec_add_ms(&deadline, duration_ms ? duration_ms : 86400000UL); > + > + do { > + tlob_busy_work(200 * 1000000UL); /* 200 ms per iteration */ > + clock_gettime(CLOCK_MONOTONIC, &now); > + } while (timespec_before(&now, &deadline)); > + > + return 0; > +}