All of lore.kernel.org
 help / color / mirror / Atom feed
From: Kris Van Hees <kris.van.hees@oracle.com>
To: dtrace@lists.linux.dev, dtrace-devel@oss.oracle.com
Subject: [PATCH 17/20] uprobe: add support for uprobe systemwide probes
Date: Fri, 30 Oct 2026 19:36:59 +0000	[thread overview]
Message-ID: <3a4910515ffe2cc8b3f3f7bdefda3444@oracle.com> (raw)

Signed-off-by: Kris Van Hees <kris.van.hees@oracle.com>
---
 libdtrace/dt_prov_uprobe.c                    | 170 ++++++++++-
 test/triggers/Build                           |   9 +-
 test/triggers/uprobe-wildcards.c              | 122 ++++++++
 .../providers/uprobe/tst.dlib-path.sh         |  70 +++++
 test/unittest/providers/uprobe/tst.dlopen.sh  | 135 +++++++++
 .../providers/uprobe/tst.elf-offsets.sh       | 278 ++++++++++++++++++
 .../providers/uprobe/tst.lifecycle.sh         | 134 +++++++++
 test/unittest/providers/uprobe/tst.multicpu.r |   4 +
 .../providers/uprobe/tst.multicpu.r.p         |   8 +
 .../unittest/providers/uprobe/tst.multicpu.sh | 138 +++++++++
 test/unittest/providers/uprobe/tst.multicpu.x |   9 +
 test/unittest/providers/uprobe/tst.multipid.r |  20 ++
 .../providers/uprobe/tst.multipid.r.p         |   4 +
 .../unittest/providers/uprobe/tst.multipid.sh | 117 ++++++++
 .../providers/uprobe/tst.multithread.sh       | 160 ++++++++++
 .../providers/uprobe/tst.probespec.sh         |  68 +++++
 .../providers/uprobe/tst.search-path.sh       |  91 ++++++
 test/unittest/providers/uprobe/tst.shlib.sh   |  81 +++++
 test/unittest/providers/uprobe/tst.symbols.sh |  52 ++++
 .../providers/uprobe/tst.systemwide.sh        | 158 ++++++++++
 20 files changed, 1826 insertions(+), 2 deletions(-)
 create mode 100644 test/triggers/uprobe-wildcards.c
 create mode 100755 test/unittest/providers/uprobe/tst.dlib-path.sh
 create mode 100755 test/unittest/providers/uprobe/tst.dlopen.sh
 create mode 100755 test/unittest/providers/uprobe/tst.elf-offsets.sh
 create mode 100755 test/unittest/providers/uprobe/tst.lifecycle.sh
 create mode 100644 test/unittest/providers/uprobe/tst.multicpu.r
 create mode 100755 test/unittest/providers/uprobe/tst.multicpu.r.p
 create mode 100755 test/unittest/providers/uprobe/tst.multicpu.sh
 create mode 100755 test/unittest/providers/uprobe/tst.multicpu.x
 create mode 100644 test/unittest/providers/uprobe/tst.multipid.r
 create mode 100755 test/unittest/providers/uprobe/tst.multipid.r.p
 create mode 100755 test/unittest/providers/uprobe/tst.multipid.sh
 create mode 100755 test/unittest/providers/uprobe/tst.multithread.sh
 create mode 100755 test/unittest/providers/uprobe/tst.probespec.sh
 create mode 100755 test/unittest/providers/uprobe/tst.search-path.sh
 create mode 100755 test/unittest/providers/uprobe/tst.shlib.sh
 create mode 100755 test/unittest/providers/uprobe/tst.symbols.sh
 create mode 100755 test/unittest/providers/uprobe/tst.systemwide.sh

diff --git a/libdtrace/dt_prov_uprobe.c b/libdtrace/dt_prov_uprobe.c
index 1d2b9755..97276fe0 100644
--- a/libdtrace/dt_prov_uprobe.c
+++ b/libdtrace/dt_prov_uprobe.c
@@ -37,6 +37,7 @@
 #include "dt_dctx.h"
 #include "dt_cg.h"
 #include "dt_list.h"
+#include "dt_module.h"
 #include "dt_provider_tp.h"
 #include "dt_probe.h"
 #include "dt_program.h"
@@ -401,6 +402,161 @@ static int populate_usdt(dtrace_hdl_t *dtp)
 	return 0;
 }
 
+/* Create a concrete probe, retaining existing probes across provide calls. */
+static int provide_uprobe(dtrace_hdl_t *dtp, dt_provider_t *prv,
+			  dt_module_t *dmp, const char *fun, uint64_t off,
+			  int flags)
+{
+	dtrace_probedesc_t pd = {
+		.prv = prv->desc.dtvd_name,
+		.mod = dmp->dm_name,
+		.fun = fun,
+		.prb = flags & PP_IS_RETURN ? "return" : "entry",
+	};
+	dt_uprobe_t	*upp;
+
+	if (dt_probe_lookup(dtp, &pd) != NULL)
+		return 0;
+
+	upp = dt_zalloc(dtp, sizeof(dt_uprobe_t));
+	if (upp == NULL)
+		return 0;
+
+	upp->fn = strdup(dmp->dm_file);
+	if (upp->fn == NULL) {
+		dt_free(dtp, upp);
+		return 0;
+	}
+	upp->off = off;
+	upp->fd = -1;
+	upp->pid = -1;
+	upp->flags = flags;
+
+	if (dt_probe_insert(dtp, prv, pd.prv, pd.mod, pd.fun, pd.prb,
+			    upp) != NULL)
+		return 1;
+
+	dt_free(dtp, upp->fn);
+	dt_free(dtp, upp);
+	return 0;
+}
+
+/* Probe kinds selected by provide(), independent of per-probe PP_IS_* flags. */
+#define PROVIDE_ENTRY	0x1
+#define PROVIDE_RETURN	0x2
+
+/* Match defined functions in both regular and dynamic ELF symbol tables. */
+static int provide_module(dtrace_hdl_t *dtp, dt_provider_t *prv,
+			  dt_module_t *dmp, const dtrace_probedesc_t *pdp,
+			  int flags)
+{
+	dtrace_objinfo_t	info;
+	Elf_Scn		*scn = NULL;
+	int		n = 0;
+
+	if ((dmp->dm_flags & DT_DM_KERNEL) || dmp->dm_file[0] == '\0' ||
+	    dtrace_object_info(dtp, dmp->dm_name, &info) == -1 ||
+	    dmp->dm_elf == NULL)
+		return 0;
+
+	while ((scn = elf_nextscn(dmp->dm_elf, scn)) != NULL) {
+		GElf_Shdr	sh;
+		Elf_Data	*data;
+		size_t		i;
+
+		if (gelf_getshdr(scn, &sh) == NULL ||
+		    (sh.sh_type != SHT_SYMTAB && sh.sh_type != SHT_DYNSYM) ||
+		    sh.sh_entsize == 0 || (data = elf_getdata(scn, NULL)) == NULL)
+			continue;
+
+		for (i = 0; i < sh.sh_size / sh.sh_entsize; i++) {
+			GElf_Sym	sym;
+			GElf_Shdr	fsh;
+			Elf_Scn		*fscn;
+			const char	*fun;
+			uint64_t	off;
+
+			if (gelf_getsym(data, i, &sym) == NULL ||
+			    GELF_ST_TYPE(sym.st_info) != STT_FUNC ||
+			    sym.st_shndx == SHN_UNDEF ||
+			    sym.st_shndx >= SHN_LORESERVE)
+				continue;
+
+			fun = elf_strptr(dmp->dm_elf, sh.sh_link, sym.st_name);
+			if (fun == NULL || *fun == '\0' ||
+			    !dt_gmatch(fun, pdp->fun))
+				continue;
+
+			/* perf uprobes take file offsets, not virtual addresses. */
+			fscn = elf_getscn(dmp->dm_elf, sym.st_shndx);
+			if (fscn == NULL || gelf_getshdr(fscn, &fsh) == NULL ||
+			    !(fsh.sh_flags & SHF_EXECINSTR) ||
+			    fsh.sh_type == SHT_NOBITS || sym.st_value < fsh.sh_addr ||
+			    sym.st_value - fsh.sh_addr >= fsh.sh_size)
+				continue;
+			off = sym.st_value - fsh.sh_addr + fsh.sh_offset;
+
+			if (flags & PROVIDE_ENTRY)
+				n += provide_uprobe(dtp, prv, dmp, fun, off, 0);
+			if (flags & PROVIDE_RETURN)
+				n += provide_uprobe(dtp, prv, dmp, fun, off,
+						    PP_IS_RETURN);
+		}
+	}
+
+	return n;
+}
+
+typedef struct provide_module_arg {
+	dt_provider_t		*prv;
+	const dtrace_probedesc_t	*pdp;
+	int			flags;
+	int			n;
+} provide_module_arg_t;
+
+static int provide_module_cb(dtrace_hdl_t *dtp, dt_module_t *dmp, void *arg)
+{
+	provide_module_arg_t	*data = arg;
+
+	data->n += provide_module(dtp, data->prv, dmp, data->pdp, data->flags);
+	return 0;
+}
+
+static int provide(dtrace_hdl_t *dtp, const dtrace_probedesc_t *pdp)
+{
+	dt_provider_t	*prv = dt_provider_lookup(dtp, pdp->prv);
+	dt_module_t	*dmp;
+	provide_module_arg_t arg = { .prv = prv, .pdp = pdp };
+
+	if (prv == NULL)
+		return 0;
+
+	/* Disallow uprobe probe specifications with empty module component. */
+	if (pdp->mod && pdp->mod[0] == 0)
+		return 0;
+
+	if (dt_gmatch("entry", pdp->prb))
+		arg.flags |= PROVIDE_ENTRY;
+	if (dt_gmatch("return", pdp->prb))
+		arg.flags |= PROVIDE_RETURN;
+	if (arg.flags == 0)
+		return 0;
+
+	if (pdp->mod[0] != '\0' && !strisglob(pdp->mod)) {
+		dmp = dt_module_lookup_by_name(dtp, pdp->mod);
+		if (dmp == NULL)
+			dmp = dt_module_create_user(dtp, pdp->mod);
+		if (dmp == NULL)
+			return 0;
+
+		return provide_module(dtp, prv, dmp, pdp, arg.flags);
+	}
+
+	dt_module_user_iter(dtp, pdp->mod, provide_module_cb, &arg);
+
+	return arg.n;
+}
+
 static void free_probe_list(dtrace_hdl_t *dtp, list_probe_t *elem)
 {
 	while (elem != NULL) {
@@ -1390,6 +1546,16 @@ static int trampoline(dt_pcb_t *pcb, uint_t exitlbl)
 	 */
 	dt_cg_tramp_copy_regs(pcb);
 
+	if (!dt_list_empty(&uprp->stmts)) {
+		/* Populate probe arguments.  */
+		if (upp->flags & PP_IS_RETURN)
+			dt_cg_tramp_copy_rval_from_regs(pcb);
+		else
+			dt_cg_tramp_copy_args_from_regs(pcb, 1);
+
+		dt_cg_tramp_call_clauses(pcb, uprp, DT_ACTIVITY_ACTIVE);
+	}
+
 	/*
 	 * pid probes.
 	 *
@@ -1542,7 +1708,8 @@ static int uprobe_create(dtrace_hdl_t *dtp, const dt_uprobe_t *upp,
 	attr.uprobe_path = (uint64_t)upp->fn;
 	attr.probe_offset = upp->off;
 
-	return dt_perf_event_open(&attr, upp->pid, -1, -1, 0);
+	return dt_perf_event_open(&attr, upp->pid, upp->pid == -1 ? 0 : -1,
+				  -1, 0);
 }
 
 static int attach(dtrace_hdl_t *dtp, const dt_probe_t *uprp, int bpf_fd)
@@ -1720,6 +1887,7 @@ dt_provimpl_t	dt_uprobe = {
 	.name		= prvname,
 	.prog_type	= BPF_PROG_TYPE_KPROBE,
 	.populate	= &populate,
+	.provide	= &provide,
 	.load_prog	= &dt_bpf_prog_load,
 	.trampoline	= &trampoline,
 	.attach		= &attach,
diff --git a/test/triggers/Build b/test/triggers/Build
index 0dd546ac..fed3474b 100644
--- a/test/triggers/Build
+++ b/test/triggers/Build
@@ -25,7 +25,8 @@ EXTERNAL_TRIGGERS = $(EXTERNAL_64BIT_TRIGGERS) $(if $(NATIVE_BITNESS_ONLY),,$(EX
 INTERNAL_64BIT_TRIGGERS = libproc-pldd libproc-consistency libproc-sleeper \
     libproc-sleeper-pie libproc-dlmadopen libproc-lookup-by-name \
     libproc-lookup-victim libproc-execing-bkpts libproc-execing-bkpts-victim \
-    btf-cyclic-types btf-lazy-types shared-ctf module-discovery elf-symbols
+    btf-cyclic-types btf-lazy-types shared-ctf module-discovery elf-symbols \
+    uprobe-wildcards
 INTERNAL_32BIT_TRIGGERS := libproc-sleeper-32 libproc-sleeper-pie-32
 INTERNAL_TRIGGERS = $(INTERNAL_64BIT_TRIGGERS) $(if $(NATIVE_BITNESS_ONLY),,$(INTERNAL_32BIT_TRIGGERS))
 
@@ -258,6 +259,12 @@ ustack-tst-spin_CFLAGS := -O0
 ustack-tst-mtspin_CFLAGS := -O0
 ustack-tst-mtspin_LIBS := -lpthread
 
+uprobe-wildcards_CFLAGS := -Ilibproc -Ilibdtrace -Ilibcommon -Wno-pedantic
+uprobe-wildcards_LDFLAGS := -Wl,--export-dynamic
+uprobe-wildcards_DEPS := build-libdtrace.a build-libproc.a libcommon.a libport.a
+uprobe-wildcards_LIBS := $(objdir)/build-libdtrace.a $(objdir)/build-libproc.a \
+    $(objdir)/build-libcommon.a $(objdir)/build-libport.a $(libdtrace_LIBS)
+
 module-discovery_CFLAGS := -Ilibproc -Ilibdtrace -Ilibcommon -Wno-pedantic
 module-discovery_LDFLAGS := -Wl,--wrap=fopen64 \
     -Wl,--wrap=dt_bpf_btf_get_next_id -Wl,--wrap=dt_bpf_btf_get_fd_by_id \
diff --git a/test/triggers/uprobe-wildcards.c b/test/triggers/uprobe-wildcards.c
new file mode 100644
index 00000000..d8815ce5
--- /dev/null
+++ b/test/triggers/uprobe-wildcards.c
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+ * Licensed under the Universal Permissive License v 1.0 as shown at
+ * http://oss.oracle.com/licenses/upl.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dt_module.h>
+#include <dt_probe.h>
+#include <dt_provider.h>
+
+#define CHECK(expr) do { \
+	if (!(expr)) { \
+		fprintf(stderr, "Check failed at line %d: %s\n", __LINE__, #expr); \
+		return 1; \
+	} \
+} while (0)
+
+/* Export these to exercise both .symtab and stripped .dynsym objects. */
+int uprobe_test_data;
+int uprobe_test_alpha(void) { return 1; }
+int uprobe_test_beta(void) { return 2; }
+
+static int
+count_probe(dtrace_hdl_t *dtp, const dtrace_probedesc_t *pdp, void *arg)
+{
+	int *count = arg;
+
+	(*count)++;
+	return 0;
+}
+
+typedef struct module_count {
+	int count;
+	int result;
+} module_count_t;
+
+static int
+count_module(dtrace_hdl_t *dtp, dt_module_t *dmp, void *arg)
+{
+	module_count_t *data = arg;
+
+	data->count++;
+	return data->result;
+}
+
+int
+main(int argc, char **argv)
+{
+	dtrace_hdl_t	*dtp;
+	dt_probe_t	*prp;
+	int		err, count = 0;
+	module_count_t	modules = { 0 };
+	dtrace_probedesc_t pd = {
+		.prv = "uprobe", .mod = "uprobe-wildcards-a",
+		.fun = "uprobe_test_*", .prb = "*",
+	};
+
+	CHECK(argc == 2);
+	CHECK(setenv("LD_LIBRARY_PATH", argv[1], 1) == 0);
+	CHECK((dtp = dtrace_open(DTRACE_VERSION, 0, &err)) != NULL);
+	dt_probe_init(dtp);
+	CHECK(dt_uprobe.populate(dtp) == 0);
+
+	CHECK(dt_uprobe.provide(dtp, &pd) == 4);
+	CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+	pd.mod = "uprobe-wildcards-[ab]";
+	CHECK(dt_uprobe.provide(dtp, &pd) == 4);
+	pd.mod = "uprobe-wildcards-?";
+	CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+	pd.prv = "uprob?";
+	pd.fun = "uprobe_test_?lpha";
+	pd.prb = "[er]*";
+	CHECK(dtrace_probe_iter(dtp, &pd, count_probe, &count) == 0);
+	CHECK(count == 4);
+	pd.prv = "uprobe";
+
+	/* Visit cached modules once each and honor callback termination. */
+	CHECK(dt_module_user_iter(dtp, "uprobe-wildcards-[ab]",
+	    count_module, &modules) == 0);
+	CHECK(modules.count == 2);
+	modules.count = 0;
+	modules.result = 7;
+	CHECK(dt_module_user_iter(dtp, "uprobe-wildcards-[ab]",
+	    count_module, &modules) == 7);
+	CHECK(modules.count == 1);
+	modules.count = 0;
+	CHECK(dt_module_user_iter(dtp, "uprobe-no-such-module-*",
+	    count_module, &modules) == 0);
+	CHECK(modules.count == 0);
+
+	pd.mod = "uprobe-wildcards-b";
+	pd.fun = "uprobe_test_alpha";
+	pd.prb = "entry";
+	CHECK((prp = dt_probe_lookup(dtp, &pd)) != NULL);
+	CHECK(strcmp(prp->desc->mod, pd.mod) == 0);
+	CHECK(strcmp(prp->desc->fun, pd.fun) == 0);
+	pd.prb = "return";
+	CHECK(dt_probe_lookup(dtp, &pd) != NULL);
+	pd.fun = "uprobe_test_data";
+	CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+	CHECK(dt_probe_lookup(dtp, &pd) == NULL);
+	pd.fun = "fprintf";
+	CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+	pd.prb = "no-such-probe";
+	CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+
+	/* Empty components match everything, including both probe names. */
+	pd.mod = "uprobe-wildcards-b";
+	pd.fun = "";
+	pd.prb = "";
+	CHECK(dt_uprobe.provide(dtp, &pd) > 0);
+	CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+	pd.mod = "";
+	pd.fun = "uprobe_test_?lpha";
+	CHECK(dt_uprobe.provide(dtp, &pd) >= 0);
+
+	dtrace_close(dtp);
+	return 0;
+}
diff --git a/test/unittest/providers/uprobe/tst.dlib-path.sh b/test/unittest/providers/uprobe/tst.dlib-path.sh
new file mode 100755
index 00000000..6c457daa
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.dlib-path.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: Specifying -L does not break system-wide uprobe probes.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+NWORKERS=20
+DIRNAME="$tmpdir/uprobe-dlib-path.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+churn_probe(long value)
+{
+	return value + 1;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <stdlib.h>
+
+extern long churn_probe(long);
+
+int
+main(int argc, char **argv)
+{
+	long value, got;
+
+	if (argc != 2)
+		return 2;
+
+	value = strtol(argv[1], NULL, 0);
+	got = churn_probe(value);
+
+	return got == value + 1 ? 0 : 3;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswchurn.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswchurn || exit 1
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+$dtrace $dt_flags -L/nothing -c './worker 1' -qn '
+uprobe:libswchurn.so:churn_probe:entry
+{
+	printf("%ld = PID %d\n", arg0, pid);
+	exit(0);
+}
+
+profile:::tick-1s
+{
+	exit(1);
+}
+'
+
+exit $?
diff --git a/test/unittest/providers/uprobe/tst.dlopen.sh b/test/unittest/providers/uprobe/tst.dlopen.sh
new file mode 100755
index 00000000..04a281af
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.dlopen.sh
@@ -0,0 +1,135 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe fires for a library that is dlopen()ed only
+# after tracing has started.
+#
+# @@timeout: 30
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-dlopen.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+lazy_probe(long value)
+{
+	return value * 3 + 1;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <dlfcn.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+typedef long (*probe_f)(long);
+
+int
+main(int argc, char **argv)
+{
+	char ch;
+	int fd;
+	long got;
+	void *hdl;
+	probe_f probe;
+
+	if (argc != 2)
+		return 2;
+
+	fd = open(argv[1], O_RDONLY);
+	if (fd == -1)
+		return 3;
+	if (read(fd, &ch, 1) == -1 && errno != EINTR)
+		return 4;
+	close(fd);
+
+	hdl = dlopen("libswlazy.so", RTLD_NOW);
+	if (hdl == NULL) {
+		fprintf(stderr, "dlopen failed: %s\n", dlerror());
+		return 5;
+	}
+
+	probe = (probe_f)dlsym(hdl, "lazy_probe");
+	if (probe == NULL) {
+		fprintf(stderr, "dlsym failed: %s\n", dlerror());
+		return 6;
+	}
+
+	got = probe(17);
+	dlclose(hdl);
+
+	return got == 52 ? 0 : 7;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswlazy.so probe.c || exit 1
+if ! ${CC:-cc} $test_cppflags -o worker worker.c -ldl; then
+	${CC:-cc} $test_cppflags -o worker worker.c || exit 1
+fi
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+mkfifo gate
+
+dtrace_pid=
+worker_pid=
+cleanup()
+{
+	[ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+	[ -n "$worker_pid" ] && kill "$worker_pid" 2>/dev/null
+}
+trap cleanup EXIT
+
+./worker gate &
+worker_pid=$!
+
+# $dtrace $dt_flags -qn '
+$dtrace $dt_flags -n '
+uprobe:libswlazy.so:lazy_probe:entry
+{
+	self->pid = pid;
+	self->expected = arg0 * 3 + 1;
+	printf("E %ld\n", arg0);
+}
+
+uprobe:libswlazy.so:lazy_probe:return
+/self->pid == pid/
+{
+	printf("R %ld (expected %ld)\n", arg1, self->expected);
+	exit(self->expected == arg1 ? 0 : 1);
+}
+
+profile:::tick-5s
+{
+	exit(1);
+}
+' > dtrace.out 2> dtrace.err &
+dtrace_pid=$!
+
+sleep 2
+printf x > gate
+
+wait "$worker_pid" || exit 1
+worker_pid=
+
+if ! wait "$dtrace_pid"; then
+	cat dtrace.err
+	cat dtrace.out
+	exit 1
+fi
+dtrace_pid=
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.elf-offsets.sh b/test/unittest/providers/uprobe/tst.elf-offsets.sh
new file mode 100755
index 00000000..af9fe16f
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.elf-offsets.sh
@@ -0,0 +1,278 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: System-wide uprobes use correct file offsets for shared objects,
+# PIE executables, and non-PIE executables when the toolchain supports them.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-elf-offsets.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > libprobe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+lib_offset_probe(long value)
+{
+	return value + 101;
+}
+EOF
+
+cat > libworker.c <<'EOF'
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+extern long lib_offset_probe(long);
+
+static int
+wait_for_gate(const char *path)
+{
+	char ch;
+	int fd = open(path, O_RDONLY);
+
+	if (fd == -1)
+		return 1;
+	if (read(fd, &ch, 1) == -1 && errno != EINTR)
+		return 1;
+	close(fd);
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	long value, got;
+
+	if (argc != 3)
+		return 2;
+	if (wait_for_gate(argv[1]) != 0)
+		return 3;
+
+	value = strtol(argv[2], NULL, 0);
+	got = lib_offset_probe(value);
+
+	return got == value + 101 ? 0 : 4;
+}
+EOF
+
+cat > pieprog.c <<'EOF'
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+__attribute__((noinline, visibility("default")))
+long
+pie_offset_probe(long value)
+{
+	return value + 202;
+}
+
+static int
+wait_for_gate(const char *path)
+{
+	char ch;
+	int fd = open(path, O_RDONLY);
+
+	if (fd == -1)
+		return 1;
+	if (read(fd, &ch, 1) == -1 && errno != EINTR)
+		return 1;
+	close(fd);
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	long value, got;
+
+	if (argc != 3)
+		return 2;
+	if (wait_for_gate(argv[1]) != 0)
+		return 3;
+
+	value = strtol(argv[2], NULL, 0);
+	got = pie_offset_probe(value);
+
+	return got == value + 202 ? 0 : 4;
+}
+EOF
+
+cat > nopieprog.c <<'EOF'
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+__attribute__((noinline, visibility("default")))
+long
+nopie_offset_probe(long value)
+{
+	return value + 303;
+}
+
+static int
+wait_for_gate(const char *path)
+{
+	char ch;
+	int fd = open(path, O_RDONLY);
+
+	if (fd == -1)
+		return 1;
+	if (read(fd, &ch, 1) == -1 && errno != EINTR)
+		return 1;
+	close(fd);
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	long value, got;
+
+	if (argc != 3)
+		return 2;
+	if (wait_for_gate(argv[1]) != 0)
+		return 3;
+
+	value = strtol(argv[2], NULL, 0);
+	got = nopie_offset_probe(value);
+
+	return got == value + 303 ? 0 : 4;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libuoffset.so libprobe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o libworker libworker.c -luoffset || exit 1
+${CC:-cc} $test_cppflags -fPIE -pie -o pieprog pieprog.c || exit 1
+
+have_nopie=0
+if ${CC:-cc} $test_cppflags -fno-pie -no-pie -o nopieprog nopieprog.c; then
+	have_nopie=1
+else
+	echo "non-PIE executable build failed: non-PIE subtest skipped"
+fi
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+export PATH="$PWD${PATH:+:$PATH}"
+mkfifo gate
+
+cat > trace.d <<'EOF'
+uprobe:libuoffset.so:lib_offset_probe:entry
+{
+	libhit = 1;
+	printf("lib %d %ld\n", pid, arg0);
+}
+
+uprobe:pieprog:pie_offset_probe:entry
+{
+	piehit = 1;
+	printf("pie %d %ld\n", pid, arg0);
+}
+EOF
+
+if [ "$have_nopie" = 1 ]; then
+	cat >> trace.d <<'EOF'
+
+uprobe:nopieprog:nopie_offset_probe:entry
+{
+	nopiehit = 1;
+	printf("nopie %d %ld\n", pid, arg0);
+}
+EOF
+fi
+
+cat >> trace.d <<EOF
+
+profile:::tick-1s
+EOF
+if [ "$have_nopie" = 1 ]; then
+	cat >> trace.d <<'EOF'
+/libhit && piehit && nopiehit/
+EOF
+else
+	cat >> trace.d <<'EOF'
+/libhit && piehit/
+EOF
+fi
+cat >> trace.d <<'EOF'
+{
+	exit(0);
+}
+
+profile:::tick-12s
+{
+	exit(1);
+}
+EOF
+
+dtrace_pid=
+pids=
+cleanup()
+{
+	[ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+	[ -n "$pids" ] && kill $pids 2>/dev/null
+}
+trap cleanup EXIT
+
+./libworker gate 10 & pids="$pids $!"
+./pieprog gate 20 & pids="$pids $!"
+if [ "$have_nopie" = 1 ]; then
+	./nopieprog gate 30 & pids="$pids $!"
+fi
+
+$dtrace $dt_flags -qs trace.d > dtrace.out 2> dtrace.err &
+dtrace_pid=$!
+
+sleep 2
+expected=2
+[ "$have_nopie" = 1 ] && expected=3
+i=0
+while [ $i -lt $expected ]; do
+	printf x
+	i=$((i + 1))
+done > gate
+
+for pid in $pids; do
+	wait "$pid" || exit 1
+done
+pids=
+
+if ! wait "$dtrace_pid"; then
+	cat dtrace.err
+	cat dtrace.out
+	exit 1
+fi
+dtrace_pid=
+
+for name in lib pie; do
+	if ! grep -q "^$name " dtrace.out; then
+		echo "ERROR: missing $name uprobe hit"
+		cat dtrace.err
+		cat dtrace.out
+		exit 1
+	fi
+done
+if [ "$have_nopie" = 1 ] && ! grep -q '^nopie ' dtrace.out; then
+	echo "ERROR: missing nopie uprobe hit"
+	cat dtrace.err
+	cat dtrace.out
+	exit 1
+fi
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.lifecycle.sh b/test/unittest/providers/uprobe/tst.lifecycle.sh
new file mode 100755
index 00000000..2c38a235
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.lifecycle.sh
@@ -0,0 +1,134 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe can be enabled, fired, detached, and enabled
+# again repeatedly for the same ELF object.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-lifecycle.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+life_probe(long value)
+{
+	return value + 1;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+extern long life_probe(long);
+
+static int
+wait_for_gate(const char *path)
+{
+	char ch;
+	int fd = open(path, O_RDONLY);
+
+	if (fd == -1)
+		return 1;
+	if (read(fd, &ch, 1) == -1 && errno != EINTR)
+		return 1;
+	close(fd);
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	long value, got;
+
+	if (argc != 3)
+		return 2;
+
+	value = strtol(argv[1], NULL, 0);
+	if (wait_for_gate(argv[2]) != 0)
+		return 3;
+
+	got = life_probe(value);
+
+	return got == value + 1 ? 0 : 4;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswlife.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswlife || exit 1
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+dtrace_pid=
+worker_pid=
+cleanup()
+{
+	[ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+	[ -n "$worker_pid" ] && kill "$worker_pid" 2>/dev/null
+}
+trap cleanup EXIT
+
+i=1
+while [ $i -le 5 ]; do
+	gate="gate.$i"
+	mkfifo "$gate"
+	./worker "$i" "$gate" &
+	worker_pid=$!
+
+	$dtrace $dt_flags -qn '
+uprobe:libswlife.so:life_probe:entry
+{
+	printf("%ld\n", arg0);
+	exit(arg0 != $1);
+}
+
+profile:::tick-8s
+{
+	exit(1);
+}
+' $i > "dtrace.$i.out" 2> "dtrace.$i.err" &
+	dtrace_pid=$!
+
+	sleep 2
+	printf x > "$gate"
+
+	if ! wait "$worker_pid"; then
+		cat "dtrace.$i.err"
+		cat "dtrace.$i.out"
+		exit 1
+	fi
+	worker_pid=
+
+	if ! wait "$dtrace_pid"; then
+		cat "dtrace.$i.err"
+		cat "dtrace.$i.out"
+		exit 1
+	fi
+	dtrace_pid=
+
+	if ! grep -qx "$i" "dtrace.$i.out"; then
+		echo "ERROR: iteration $i did not report the expected argument"
+		cat "dtrace.$i.err"
+		cat "dtrace.$i.out"
+		exit 1
+	fi
+
+	i=$((i + 1))
+done
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.multicpu.r b/test/unittest/providers/uprobe/tst.multicpu.r
new file mode 100644
index 00000000..8f30a28f
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multicpu.r
@@ -0,0 +1,4 @@
+1st CPU is A
+2nd CPU is B
+cpu A
+cpu B
diff --git a/test/unittest/providers/uprobe/tst.multicpu.r.p b/test/unittest/providers/uprobe/tst.multicpu.r.p
new file mode 100755
index 00000000..89a1dbb4
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multicpu.r.p
@@ -0,0 +1,8 @@
+#!/usr/bin/gawk -f
+
+# 1st CPU is %d
+/^1st/ { cpu_a = $NF; $NF = "A"; print; next; }
+# 2nd CPU is %d
+/^2nd/ { cpu_b = $NF; $NF = "B"; print; next; }
+# cpu %d
+/^cpu/ { $NF = ($NF == cpu_a) ? "A" : ($NF == cpu_b) ? "B" : "X"; print; }
diff --git a/test/unittest/providers/uprobe/tst.multicpu.sh b/test/unittest/providers/uprobe/tst.multicpu.sh
new file mode 100755
index 00000000..9a4760ab
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multicpu.sh
@@ -0,0 +1,138 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe can fires on every CPU allowed by the current
+#	     cpuset.
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-allcpus.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+#include <unistd.h>
+
+__attribute__((noinline, visibility("default")))
+void
+cpu_probe(unsigned long value)
+{
+	__asm__ volatile("" : : "r" (value) : "memory");
+}
+
+void
+run_probes(void)
+{
+	int i;
+
+	for (i = 0; i < 100; i++) {
+		cpu_probe((unsigned long)i);
+		usleep(10000);
+	}
+}
+EOF
+
+cat > worker.c <<'EOF'
+extern void run_probes(void);
+int main(void) { run_probes(); return 0; }
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswcpu.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswcpu || exit 1
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+# Expand the affinity list and choose up to two actual allowed CPUs.  This is
+# cpuset-safe and intentionally does not assume that CPU 0 is available.
+cpuspec=$(awk '/^Cpus_allowed_list:/ { print $2 }' /proc/self/status)
+cpus=()
+IFS=',' read -ra ranges <<< "$cpuspec"
+for range in "${ranges[@]}"; do
+	if [[ $range == *-* ]]; then
+		lo=${range%-*}; hi=${range#*-}
+		for ((cpu = lo; cpu <= hi; cpu++)); do
+			cpus+=("$cpu")
+		done
+	else
+		cpus+=("$range")
+	fi
+done
+
+[ ${#cpus[@]} -gt 0 ] || { echo ERROR: no allowed CPUs; exit 1; }
+[ ${#cpus[@]} -gt 2 ] || { echo SKIPPED: not enough CPUs; exit 2; }
+
+# We do not want to use CPU 0 because that is specified in perf_event_open()
+cpu_a=${cpus[1]}
+cpu_b=${cpus[${#cpus[@]}-1]}
+
+dtrace_pid=
+workers=
+cleanup()
+{
+	[ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+	[ -n "$workers" ] && kill $workers 2>/dev/null
+}
+trap cleanup EXIT
+
+$dtrace $dt_flags -qn '
+dtrace:::BEGIN {
+	printf("1st CPU is %d\n2nd CPU is %d\n", $1, $2);
+}
+
+uprobe:libswcpu.so:cpu_probe:entry
+/seen[cpu] == 0/
+{
+	seen[cpu] = 1;
+	printf("cpu %d\n", cpu);
+}
+
+uprobe:libswcpu.so:cpu_probe:entry
+/seen[$1] > 0 && seen[$2] > 0/
+{
+	exit(0);
+}
+
+profile:::tick-12s
+{
+	exit(1);
+}
+' $cpu_a $cpu_b > dtrace.out &
+dtrace_pid=$!
+
+sleep 2
+taskset -c "$cpu_a" ./worker & w1=$!; workers="$workers $w1"
+if [ "$cpu_b" != "$cpu_a" ]; then
+	taskset -c "$cpu_b" ./worker & w2=$!; workers="$workers $w2"
+fi
+
+wait "$w1" || exit 1
+if [ -n "${w2:-}" ]; then
+	wait "$w2" || exit 1
+fi
+workers=
+
+mv $DIRNAME /tmp/check-it
+
+if ! wait "$dtrace_pid"; then
+	cat dtrace.out
+	exit 1
+fi
+dtrace_pid=
+
+cat dtrace.out
+for cpu in "$cpu_a" "$cpu_b"; do
+	if ! grep -qx "cpu $cpu" dtrace.out; then
+		echo "ERROR: no uprobe fired on allowed CPU $cpu"
+		exit 1
+	fi
+done
+
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.multicpu.x b/test/unittest/providers/uprobe/tst.multicpu.x
new file mode 100755
index 00000000..1a1a05c8
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multicpu.x
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+# Skip test if taskset is not available.
+if ! command -v taskset > /dev/null; then
+	echo "taskset not found, skipping test"
+	exit 2
+fi
+
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.multipid.r b/test/unittest/providers/uprobe/tst.multipid.r
new file mode 100644
index 00000000..e2d34299
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multipid.r
@@ -0,0 +1,20 @@
+0 = PID NNN
+1 = PID NNN
+2 = PID NNN
+3 = PID NNN
+4 = PID NNN
+5 = PID NNN
+6 = PID NNN
+7 = PID NNN
+8 = PID NNN
+9 = PID NNN
+11 = PID NNN
+10 = PID NNN
+12 = PID NNN
+13 = PID NNN
+14 = PID NNN
+15 = PID NNN
+16 = PID NNN
+17 = PID NNN
+18 = PID NNN
+19 = PID NNN
diff --git a/test/unittest/providers/uprobe/tst.multipid.r.p b/test/unittest/providers/uprobe/tst.multipid.r.p
new file mode 100755
index 00000000..a78716f1
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multipid.r.p
@@ -0,0 +1,4 @@
+#!/usr/bin/gawk -f
+
+# n = PID NNN
+$3 == "PID" { $4 = "NNN"; print; }
diff --git a/test/unittest/providers/uprobe/tst.multipid.sh b/test/unittest/providers/uprobe/tst.multipid.sh
new file mode 100755
index 00000000..023092bd
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multipid.sh
@@ -0,0 +1,117 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe remains active for many short-lived
+# processes created after tracing starts.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+NWORKERS=20
+DIRNAME="$tmpdir/uprobe-multipid.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+churn_probe(long value)
+{
+	return value + 1;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <stdlib.h>
+
+extern long churn_probe(long);
+
+int
+main(int argc, char **argv)
+{
+	long value, got;
+
+	if (argc != 2)
+		return 2;
+
+	value = strtol(argv[1], NULL, 0);
+	got = churn_probe(value);
+
+	return got == value + 1 ? 0 : 3;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswchurn.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswchurn || exit 1
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+dtrace_pid=
+pids=
+cleanup()
+{
+	[ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+	[ -n "$pids" ] && kill $pids 2>/dev/null
+}
+trap cleanup EXIT
+
+$dtrace $dt_flags -qn '
+uprobe:libswchurn.so:churn_probe:entry
+/seen[pid] == 0/
+{
+	seen[pid] = 1;
+	hits++;
+	printf("%ld = PID %d\n", arg0, pid);
+}
+
+profile:::tick-1s
+/hits >= $1/
+{
+	exit(0);
+}
+
+profile:::tick-20s
+{
+	exit(1);
+}
+' $NWORKERS > dtrace.out &
+dtrace_pid=$!
+
+sleep 2
+
+i=0
+while [ $i -lt $NWORKERS ]; do
+	./worker "$i" &
+	pids="$pids $!"
+	i=$((i + 1))
+done
+
+for pid in $pids; do
+	wait "$pid" || exit 1
+done
+pids=
+
+if ! wait "$dtrace_pid"; then
+	cat dtrace.out
+	exit 1
+fi
+dtrace_pid=
+
+cat dtrace.out
+if [ "$(grep PID dtrace.out | sort -u | wc -l)" != "$NWORKERS" ]; then
+	echo "ERROR: expected $NWORKERS distinct traced processes"
+	cat dtrace.out
+	exit 1
+fi
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.multithread.sh b/test/unittest/providers/uprobe/tst.multithread.sh
new file mode 100755
index 00000000..3c44546f
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multithread.sh
@@ -0,0 +1,160 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: System-wide uprobe entry and return probes preserve per-thread
+# self state under concurrent calls from multiple threads.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+NTHREADS=4
+NITER=20
+EXPECTED=$((NTHREADS * NITER))
+DIRNAME="$tmpdir/uprobe-multithread.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+thread_probe(long thread_id, long iter)
+{
+	return thread_id * 100000 + iter;
+}
+EOF
+
+cat > worker.c <<EOF
+#include <pthread.h>
+#include <stdint.h>
+#include <unistd.h>
+
+#define NTHREADS $NTHREADS
+#define NITER $NITER
+
+extern long thread_probe(long, long);
+
+static void *
+run_thread(void *arg)
+{
+	long id = (long)(intptr_t)arg;
+	long i;
+
+	for (i = 0; i < NITER; i++) {
+		long got = thread_probe(id, i);
+
+		if (got != id * 100000 + i)
+			return (void *)1;
+		usleep(1000);
+	}
+
+	return NULL;
+}
+
+int
+main(void)
+{
+	pthread_t threads[NTHREADS];
+	long i;
+
+	for (i = 0; i < NTHREADS; i++) {
+		if (pthread_create(&threads[i], NULL, run_thread,
+		    (void *)(intptr_t)i) != 0)
+			return 2;
+	}
+
+	for (i = 0; i < NTHREADS; i++) {
+		void *status = NULL;
+
+		if (pthread_join(threads[i], &status) != 0 || status != NULL)
+			return 3;
+	}
+
+	return 0;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswthread.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswthread -pthread || exit 1
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+dtrace_pid=
+worker_pid=
+cleanup()
+{
+	[ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+	[ -n "$worker_pid" ] && kill "$worker_pid" 2>/dev/null
+}
+trap cleanup EXIT
+
+$dtrace $dt_flags -qn '
+BEGIN
+{
+	entries = 0;
+	bad = 0;
+	tids = 0;
+}
+
+uprobe:libswthread.so:thread_probe:entry
+{
+	entries++;
+	self->active = 1;
+	self->expected = arg0 * 100000 + arg1;
+}
+
+uprobe:libswthread.so:thread_probe:entry
+/seen[tid] == 0/
+{
+	seen[tid] = 1;
+	tids++;
+}
+
+uprobe:libswthread.so:thread_probe:return
+/self->active/
+{
+	returns++;
+	bad += arg1 != self->expected;
+	self->active = 0;
+}
+
+uprobe:libswthread.so:thread_probe:return
+/returns == $1/
+{
+	exit(bad != 0 || entries != $1 || tids != $2);
+}
+
+profile:::tick-20s
+{
+	exit(1);
+}
+' $EXPECTED $NTHREADS > dtrace.out 2> dtrace.err &
+dtrace_pid=$!
+
+sleep 2
+./worker &
+worker_pid=$!
+
+if ! wait "$worker_pid"; then
+	cat dtrace.err
+	cat dtrace.out
+	exit 1
+fi
+worker_pid=
+
+if ! wait "$dtrace_pid"; then
+	cat dtrace.err
+	cat dtrace.out
+	exit 1
+fi
+dtrace_pid=
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.probespec.sh b/test/unittest/providers/uprobe/tst.probespec.sh
new file mode 100755
index 00000000..ccf2f5fc
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.probespec.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: Exact and globbed system-wide uprobe descriptions create probes
+# with canonical entry and return names.
+#
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-probespec.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long named_probe(long value) { return value + 1; }
+EOF
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswpspec.so probe.c || exit 1
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+check()
+{
+	spec=$1
+	shift
+
+	if ! $dtrace $dt_flags -l -n "$spec" > list.out 2> list.err; then
+		cat list.err
+		exit 1
+	fi
+
+	awk '$2 == "uprobe" && $3 == "libswpspec.so" &&
+	    $4 == "named_probe" { print $5 }' list.out | sort -u > names.out
+	printf '%s\n' "$@" | sort -u > expected.out
+	if ! diff -u expected.out names.out; then
+		echo "ERROR: wrong probes for $spec"
+		cat list.err
+		cat list.out
+		exit 1
+	fi
+}
+
+check 'uprobe:libswpspec.so:named_probe:entry' entry
+check 'uprobe:libswpspec.so:named_probe:return' return
+check 'uprobe:libswpspec.so:named_probe:e*' entry
+check 'uprobe:libswpspec.so:named_probe:ret*' return
+check 'uprobe:libswpspec.so:named_probe:*' entry return
+check 'uprobe:libswpspec.so:named_probe:' entry return
+
+for spec in \
+    'uprobe:libswpspec.so:named_probe:offset' \
+    'uprobe:libswpspec.so:no_such_function:entry' \
+    'uprobe:no_such_module.so:named_probe:entry'; do
+	if $dtrace $dt_flags -e -n "$spec" > /dev/null 2>&1; then
+		echo "ERROR: unexpectedly matched $spec"
+		exit 1
+	fi
+done
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.search-path.sh b/test/unittest/providers/uprobe/tst.search-path.sh
new file mode 100755
index 00000000..5ee8ec9d
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.search-path.sh
@@ -0,0 +1,91 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: Userspace modules are resolved in LD_LIBRARY_PATH/PATH order,
+# including empty path components, the current directory, and relative search
+# paths.
+#
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-search-path.$$.$RANDOM"
+mkdir -p "$DIRNAME/A" "$DIRNAME/B" "$DIRNAME/bin" "$DIRNAME/sub"
+cd "$DIRNAME" || exit 1
+
+cat > a.c <<'EOF'
+__attribute__((noinline, visibility("default"))) void from_a(void) {}
+EOF
+cat > b.c <<'EOF'
+__attribute__((noinline, visibility("default"))) void from_b(void) {}
+EOF
+cat > cwd.c <<'EOF'
+__attribute__((noinline, visibility("default"))) void from_cwd(void) {}
+EOF
+cat > path.c <<'EOF'
+__attribute__((noinline, visibility("default"))) void from_path(void) {}
+int main(void) { from_path(); return 0; }
+EOF
+cat > relative.c <<'EOF'
+__attribute__((noinline, visibility("default"))) void from_relative(void) {}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o A/libchoice.so a.c || exit 1
+${CC:-cc} $test_cppflags -fPIC -shared -o B/libchoice.so b.c || exit 1
+${CC:-cc} $test_cppflags -fPIC -shared -o libcwd.so cwd.c || exit 1
+${CC:-cc} $test_cppflags -o bin/swpathprog path.c || exit 1
+${CC:-cc} $test_cppflags -fPIC -shared -o sub/librelative.so relative.c || exit 1
+
+# Retain the runner's path to libdtrace after the test's search directories.
+match()
+{
+	env_ld="$1${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+	env_path=$2
+	spec=$3
+	if ! LD_LIBRARY_PATH="$env_ld" PATH="$env_path" \
+	    "$dtrace" $dt_flags -e -n "$spec" > dtrace.out 2> dtrace.err; then
+		echo "ERROR: did not resolve $spec"
+		cat dtrace.err
+		exit 1
+	fi
+}
+
+no_match()
+{
+	env_ld="$1${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+	env_path=$2
+	spec=$3
+	if LD_LIBRARY_PATH="$env_ld" PATH="$env_path" \
+	    "$dtrace" $dt_flags -e -n "$spec" > dtrace.out 2> dtrace.err; then
+		echo "ERROR: unexpectedly resolved $spec"
+		exit 1
+	fi
+}
+
+sys_path=${PATH:-/usr/bin:/bin}
+
+# Search order must select one of two objects with the same module name.
+match "$PWD/A:$PWD/B" "$sys_path" 'uprobe:libchoice.so:from_a:entry'
+no_match "$PWD/A:$PWD/B" "$sys_path" 'uprobe:libchoice.so:from_b:entry'
+match "$PWD/B:$PWD/A" "$sys_path" 'uprobe:libchoice.so:from_b:entry'
+no_match "$PWD/B:$PWD/A" "$sys_path" 'uprobe:libchoice.so:from_a:entry'
+
+# Empty components denote '.', and duplicate components are harmless.
+match ":$PWD/A::$PWD/A" "$sys_path" 'uprobe:libcwd.so:from_cwd:entry'
+
+# Executables can be found through PATH.
+match "" "$PWD/bin:$sys_path" 'uprobe:swpathprog:from_path:entry'
+
+# Relative search paths resolve modules by their basename.
+no_match "" "$sys_path" 'uprobe:librelative.so:from_relative:entry'
+match "sub" "$sys_path" 'uprobe:librelative.so:from_relative:entry'
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.shlib.sh b/test/unittest/providers/uprobe/tst.shlib.sh
new file mode 100755
index 00000000..a4596166
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.shlib.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe probe in a shared library works.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-shlib.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+churn_probe(long value)
+{
+	return value + 1;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <stdlib.h>
+
+extern long churn_probe(long);
+
+int
+main(int argc, char **argv)
+{
+	long value, got;
+
+	if (argc != 2)
+		return 2;
+
+	value = strtol(argv[1], NULL, 0);
+	got = churn_probe(value);
+
+	return got == value + 1 ? 0 : 3;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswchurn.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswchurn || exit 1
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+if ! $dtrace $dt_flags -c "./worker 17" -qn '
+uprobe:libswchurn.so:churn_probe:entry
+{
+	printf("%ld = PID %d\n", arg0, pid);
+	exit(arg0 != 17);
+}
+
+profile:::tick-1s
+{
+	exit(1);
+}
+' > dtrace.out 2> dtrace.err; then
+	cat dtrace.err
+	cat dtrace.out
+	exit 1
+fi
+
+if ! grep -Eq '^17 = PID [0-9]+$' dtrace.out; then
+	echo "ERROR: shared-library probe did not report the expected argument"
+	cat dtrace.err
+	cat dtrace.out
+	exit 1
+fi
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.symbols.sh b/test/unittest/providers/uprobe/tst.symbols.sh
new file mode 100755
index 00000000..e46c1f76
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.symbols.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: System-wide uprobes accept defined function symbols in stripped
+# DSOs, and reject data, undefined, and nonexistent symbols.
+#
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-symbols.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > symbols.c <<'EOF'
+extern void undefined_function(void);
+__attribute__((visibility("default"))) long exported_data = 123;
+__attribute__((noinline, visibility("default"))) long exported_function(void)
+{
+	if (exported_data == -1)
+		undefined_function();
+	return exported_data;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswsymbols.so symbols.c || exit 1
+${STRIP:-strip} --strip-unneeded lib/libswsymbols.so || exit 1
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+if ! $dtrace $dt_flags -e -n \
+    'uprobe:libswsymbols.so:exported_function:entry' >/dev/null 2>&1; then
+	echo ERROR: exported function was not found in stripped DSO
+	exit 1
+fi
+
+for symbol in exported_data undefined_function does_not_exist; do
+	if $dtrace $dt_flags -e -n \
+	    "uprobe:libswsymbols.so:$symbol:entry" >/dev/null 2>&1; then
+		echo "ERROR: non-function or undefined symbol $symbol was accepted"
+		exit 1
+	fi
+done
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.systemwide.sh b/test/unittest/providers/uprobe/tst.systemwide.sh
new file mode 100755
index 00000000..e934d64d
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.systemwide.sh
@@ -0,0 +1,158 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe fires for existing and newly-created,
+# unrelated processes, and supplies entry arguments and return values.
+#
+# @@timeout: 30
+
+if [ $# != 1 ]; then
+	echo expected one argument: '<'dtrace-path'>'
+	exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-systemwide.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+sw_probe(long a, long b, long c, long d, long e, long f)
+{
+	return a + b + c + d + e + f;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+extern long sw_probe(long, long, long, long, long, long);
+
+int
+main(int argc, char **argv)
+{
+	char ch;
+	long base, got;
+	int fd;
+
+	if (argc != 3)
+		return 2;
+
+	fd = open(argv[1], O_RDONLY);
+	if (fd == -1)
+		return 3;
+	if (read(fd, &ch, 1) == -1 && errno != EINTR)
+		return 4;
+	close(fd);
+
+	base = strtol(argv[2], NULL, 0);
+	got = sw_probe(base, base + 1, base + 2, base + 3, base + 4,
+	    base + 5);
+
+	return got == base * 6 + 15 ? 0 : 5;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswuprobe.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswuprobe || exit 1
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+mkfifo gate
+
+pids=
+dtrace_pid=
+cleanup()
+{
+	[ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+	[ -n "$pids" ] && kill $pids 2>/dev/null
+}
+trap cleanup EXIT
+
+# These workers exist before DTrace attaches.  They are unrelated to DTrace
+# and wait until the tracing process has had time to enable its perf events.
+./worker gate 10 & p1=$!; pids="$pids $p1"
+./worker gate 20 & p2=$!; pids="$pids $p2"
+
+$dtrace $dt_flags -qn '
+dtrace:::BEGIN
+{
+	bad = 0;
+}
+
+uprobe:libswuprobe.so:sw_probe:entry
+{
+	entries++;
+	self->expected = arg0 + arg1 + arg2 + arg3 + arg4 + arg5;
+	bad += arg1 != arg0 + 1 || arg2 != arg0 + 2 ||
+	    arg3 != arg0 + 3 || arg4 != arg0 + 4 || arg5 != arg0 + 5;
+	bad += probemod != "libswuprobe.so" || probefunc != "sw_probe" ||
+	    probename != "entry";
+	printf("E %d %d %ld\n", pid, tid, self->expected);
+}
+
+/* A second clause must share the underlying uprobe and still be called. */
+uprobe:libswuprobe.so:sw_probe:entry
+{
+	extra_entries++;
+}
+
+uprobe:libswuprobe.so:sw_probe:return
+{
+	returns++;
+	bad += arg1 != self->expected;
+	self->expected = 0;
+	printf("R %d %d %ld\n", pid, tid, arg1);
+}
+
+uprobe:libswuprobe.so:sw_probe:return
+/ returns == 3 /
+{
+	exit(bad != 0 || entries != 3 || extra_entries != 3);
+}
+
+profile:::tick-12s
+{
+	exit(1);
+}
+' > dtrace.out 2> dtrace.err &
+dtrace_pid=$!
+
+sleep 2
+printf 'xx' > gate
+
+# This worker is created only after the probe has been enabled.
+./worker /dev/null 30 & p3=$!; pids="$pids $p3"
+
+wait "$p1" || exit 1
+wait "$p2" || exit 1
+wait "$p3" || exit 1
+pids=
+
+if ! wait "$dtrace_pid"; then
+	cat dtrace.err
+	cat dtrace.out
+	exit 1
+fi
+dtrace_pid=
+
+if [ "$(awk '$1 == "E" { n++ } END { print n + 0 }' dtrace.out)" != 3 ] ||
+   [ "$(awk '$1 == "R" { n++ } END { print n + 0 }' dtrace.out)" != 3 ] ||
+   [ "$(awk '$1 == "E" { print $2 }' dtrace.out | sort -u | wc -l)" != 3 ]; then
+	echo ERROR: expected one entry and return from each of three processes
+	cat dtrace.err
+	cat dtrace.out
+	exit 1
+fi
+
+echo success
+exit 0
-- 
2.52.0


                 reply	other threads:[~2026-10-30 19:38 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=3a4910515ffe2cc8b3f3f7bdefda3444@oracle.com \
    --to=kris.van.hees@oracle.com \
    --cc=dtrace-devel@oss.oracle.com \
    --cc=dtrace@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.