Linux DTrace development list
 help / color / mirror / Atom feed
* [PATCH 17/20] uprobe: add support for uprobe systemwide probes
@ 2026-10-30 19:36 Kris Van Hees
  0 siblings, 0 replies; only message in thread
From: Kris Van Hees @ 2026-10-30 19:36 UTC (permalink / raw)
  To: dtrace, dtrace-devel

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


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-10-30 19:38 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-30 19:36 [PATCH 17/20] uprobe: add support for uprobe systemwide probes Kris Van Hees

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox